来自公众号:code秘密花园
最近对公司的一个 PC 站点作了一次总体的性能优化,因为这个系统业务复杂、依赖很是多,加载速度很是慢,优化后各个性能指标都有了显著提高,大约加载速度快了 5 倍左右。
html

我在 构建、网络、资源加载、运行时、服务端、功能组织等多个方面都进行了优化,准备作一个系列,分章节给你们分享下个人优化经验。react
今天,咱们从优化效果最为明显的构建角度开始。webpack
优化前
首先咱们看一下在优化前站点的资源加载状况:web

可见最大的 vendor
包竟然有 3MB
(通过 gzip
压缩后),没有作额外配置的话,webpack
将全部的第三方依赖都打入了这个包,若是引入依赖愈来愈多,那么这个包就会愈来愈大。npm
另外,系统自己的逻辑打的包也达到了 600kb
编程
分析依赖关系
咱们能够借助 webpack-bundle-analyzer
将打包后的内容展现为方便交互的树状图,咱们能够很直观的看到有哪些比较大的模块,而后作针对性优化。缓存
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
CDN 引入
CDN 的工做原理是将源站的资源缓存到位于全球各地的 CDN 节点上,用户请求资源时,就近返回节点上缓存的资源,而不须要每一个用户的请求都回您的源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验。性能优化
这个估计你们都明白,由于打包后的产物自己也是上传到 CDN
的。可是咱们要作的是将体积较大的第三方依赖单独拆出来放到 CDN
上,这样这个依赖既不会占用打包资源,也不会影响最终包体积。微信
若是一个依赖有直接打包压缩好的单文件 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
外,则会在业务代码被加载以后加载,模块中使用了该模块将会报错。
拆 vendor

某些场景下, 一个第三方依赖可能拆成了多个子依赖,例如上面的 monaco
,或者没有提供可直接经过 CDN
引入的文件,咱们就没法经过配置一个 CDN
文件来引入它了。
这时咱们须要本身去 webpack
设置一些规则,将咱们想拆出来的依赖单独打包一个 vendor
。

动态 import
将 vendor
拆分后,依赖仍然会在首屏被加载,若是依赖不在首屏使用,仍然会形成网络资源的浪费,并阻塞页面渲染,对于不必在首屏进行加载的依赖,咱们能够采用动态 import
的方式。

例如上面这个 js-export-excel
这个依赖,本身自己有将近 500 kb
,可是其只会在用户点击【导出】按钮的时候使用,咱们首先在 vendor
中将其拆出来。

使用时,将 import
的逻辑由首屏改到运行时异步加载

这样的话,js-export-excel
这个依赖包只会在用户点击【导出】按钮时引入,首屏再也不引入。
不是全部依赖都适合异步加载,若是你对使用该依赖有很高的性能要求,而后依赖自己也比较大,这种状况是不适合的,由于你可能会看到明显的延迟。以上 export 实际上是一个比较合适的场景,下载 excel 自己须要延迟时间,加上动态加载依赖的时间是可接收的。
React 懒加载
相似的,对于某些第三方依赖组件,例如 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/),
]
优化效果
最终优化后,会发现模块已经被咱们拆的很是均匀,而且只会在对应页面渲染时加载对应模块,这对首屏渲染速度有显著提高。
本文分享自微信公众号 - 编程微刊(wangxiaoting678)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。