最近对公司的一个 PC 站点作了一次总体的性能优化,因为这个系统业务复杂、依赖很是多,加载速度很是慢,优化后各个性能指标都有了显著提高,大约加载速度快了 5 倍左右。javascript
我在 构建、网络、资源加载、运行时、服务端、功能组织等多个方面都进行了优化,准备作一个系列,分章节给你们分享下个人优化经验。html
今天,咱们从优化效果最为明显的构建角度开始。前端
首先咱们看一下在优化前站点的资源加载状况:java
可见最大的 vendor
包竟然有 3MB
(通过 gzip
压缩后),没有作额外配置的话,webpack
将全部的第三方依赖都打入了这个包,若是引入依赖愈来愈多,那么这个包就会愈来愈大。react
另外,系统自己的逻辑打的包也达到了 600kb
webpack
咱们能够借助 webpack-bundle-analyzer
将打包后的内容展现为方便交互的树状图,咱们能够很直观的看到有哪些比较大的模块,而后作针对性优化。web
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
复制代码
CDN 的工做原理是将源站的资源缓存到位于全球各地的 CDN 节点上,用户请求资源时,就近返回节点上缓存的资源,而不须要每一个用户的请求都回您的源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验。npm
这个估计你们都明白,由于打包后的产物自己也是上传到 CDN
的。可是咱们要作的是将体积较大的第三方依赖单独拆出来放到 CDN
上,这样这个依赖既不会占用打包资源,也不会影响最终包体积。promise
若是一个依赖有直接打包压缩好的单文件 CDN
资源,例如上面图中的 g6
,就能够直接使用。缓存
按照官方文档的解释,若是咱们想引用一个库,可是又不想让 webpack
打包,而且又不影响咱们在程序中以 import、require
或者 window/global
全局等方式进行使用,那就能够经过配置 externals
。
externals
配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所建立的bundle
依赖于那些存在于用户环境(consumer's environment)中的依赖。
首先将 CDN
引入的依赖加入到 externals
中。
而后借助 html-webpack-plugin
将 CDN
文件打入 html
:
这里有一点须要注意,在 html
中配置的 CDN
引入脚本必定要在 body
内的最底部,由于:
body
上面或 header
内,则加载会阻塞整个页面渲染。body
外,则会在业务代码被加载以后加载,模块中使用了该模块将会报错。某些场景下, 一个第三方依赖可能拆成了多个子依赖,例如上面的 monaco
,或者没有提供可直接经过 CDN
引入的文件,咱们就没法经过配置一个 CDN
文件来引入它了。
这时咱们须要本身去 webpack
设置一些规则,将咱们想拆出来的依赖单独打包一个 vendor
。
咱们来看 v8
中关于 import
的描述:
This syntactic form for importing modules is a static declaration: it only accepts a string literal as the module specifier, and introduces bindings into the local scope via a pre-runtime “linking” process. The static import syntax can only be used at the top-level of the file.
复制代码
CommonJS
和 ES Module
在用法上有个很是大的区别,CommonJS
容许你能够在用到的时候再去加载这个模块,而不用所有放到顶部加载。而 ES Module
的语法是静态的,静态 import
语法只能在文件的顶层使用。这种引用方式有个巨大缺陷,就是没法实现按需引入模块。
<script type="module">
import * as module from './utils.js';
module.default();
// → logs 'Hi from the default export!'
module.doStuff();
// → logs 'Doing stuff…'
</script>
复制代码
幸亏, ES Module
目前也支持了 Dynamic import
的用法,动态的 import
会返回一个 promise
,你能够等待模块加载完成后再去作一些事情,而不用在页面初始化就加载它。
<script type="module">
const moduleSpecifier = './utils.js';
import(moduleSpecifier)
.then((module) => {
module.default();
// → logs 'Hi from the default export!'
module.doStuff();
// → logs 'Doing stuff…'
});
</script>
<script type="module"> (async () => { const moduleSpecifier = './utils.js'; const module = await import(moduleSpecifier) module.default(); // → logs 'Hi from the default export!' module.doStuff(); // → logs 'Doing stuff…' })(); </script>
复制代码
将 vendor
拆分后,依赖仍然会在首屏被加载,若是依赖不在首屏使用,仍然会形成网络资源的浪费,并阻塞页面渲染,对于不必在首屏进行加载的依赖,咱们能够采用动态 import
的方式。
例如上面这个 js-export-excel
这个依赖,本身自己有将近 500 kb
,可是其只会在用户点击【导出】按钮的时候使用,咱们首先在 vendor
中将其拆出来。
使用时,将 import
的逻辑由首屏改到运行时异步加载
这样的话,js-export-excel
这个依赖包只会在用户点击【导出】按钮时引入,首屏再也不引入。
不是全部依赖都适合异步加载,若是你对使用该依赖有很高的性能要求,而后依赖自己也比较大,这种状况是不适合的,由于你可能会看到明显的延迟。以上 export 实际上是一个比较合适的场景,下载 excel 自己须要延迟时间,加上动态加载依赖的时间是可接收的。
相似的,对于某些第三方依赖组件,例如 monaco editor
,咱们只有在不多的业务场景下才会用到,可是其自己一个包占用了 5MB
。。咱们每次在打开页面时都要加载它,这太耗费性能了。
对于一个依赖包,咱们能够经过动态 import
的方式进行懒加载,可是对于一个 React
组件,直接使用动态 import
可能就不太合适了,组件渲染的运行时都是可屡次触发了,不可能在每次组件渲染时都加载一次组件。
React.lazy
函数能让你像渲染常规组件同样处理动态引入组件。React.lazy
接受一个函数,这个函数须要动态调用 import()
。它必须返回一个 Promise
,该 Promise
须要 resolve
一个 default export
的 React
组件。
const MonacoEditor = React.lazy(() => import('react-monaco-editor'));
复制代码
此代码将会在组件首次渲染时,自动导入包含 MonacoEditor
组件的包。可是直接使用React.lazy
引入的组件是没法直接使用的,由于 React
没法预测组件什么时候被加载,直接渲染会致使页面崩溃。
在 Suspense
组件中渲染 lazy
组件,可使用在等待加载 lazy
组件时作优雅降级(如 loading
)。fallback
属性接受任何在组件加载过程当中你想展现的 React
元素。你能够将 Suspense
组件置于懒加载组件之上的任何位置。你甚至能够用一个 Suspense
组件包裹多个懒加载组件。
将全部 monaco editor
改成懒加载后,首屏已经不会加载 monaco editor
。
上面 React
懒加载的方式,一样适用于路由,对于每一个路由都使用懒加载的方式引入,则每一个模块都会被单独打为一个 js
,首屏只会加载当前模块引入的 js
。
不过 路由懒加载 也有一个很明显的弊端,就是每一个模块的资源是只有加载这个模块的时候才回去下载的,因此在切换模块的时候可能会有一小段白屏或
loading
效果,这个要结合业务自身的状况综合判断要不要使用。
在某些场景下,语言包会占用整个包体积的很是大一部分。实际上库自己的逻辑不会很大,moment
就是一个很好例子。
若是最开始选择日期库,那直接推荐使用 dayjs
了,若是你选择了 moment
,必定要注意把不使用的语言包过滤掉,推荐使用 ContextReplacementPlugin
,它会告诉 webpack
咱们会使用到哪一个本地文件:
plugins: [
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn/),
]
复制代码
最终优化后,会发现模块已经被咱们拆的很是均匀,而且只会在对应页面渲染时加载对应模块,这对首屏渲染速度有显著提高。
文章中若有错误,欢迎在评论区指正;若是文章对你有帮助,欢迎点赞、评论、分享、但愿能帮到更多人。
本文首发于公众号《code秘密花园》欢迎你们关注,原文:我是如何将网页性能提高5倍的 — 构建优化篇
字节跳动 IES 前端架构团队急缺人才(p5/p7/p7大量HC),欢迎加我微信
ConardLi
一块儿来搞事情。