最近在接手一个项目的时候,由于其开发已经一年有余,整个项目使用了create-react-app
脚手架,Webpack版本停留在3.x,React版本15.x,并且在线上使用效果并不好,首页加载缓慢。对于这样一个典型的老项目,进行构建优化。
在
webpack 3.x
之前的构建中,该项目使用CommonChunkPlugin
对特定的package
进行了抽取,例如react
、图表库、组件库,其余使用到的package
和业务代码被打包在了一起,Gzip过后差不多1MB
。通过分析可以得知,分离出的第三方库和业务代码存在重复打包的情况,并且页面初始加载并不需要用到图标库,而组件库只使用到了部分组件却被完全打包了。
升级Webpack
由于使用的是create-react-app
脚手架,其自带了react-scripts
作为webpack
的配置,本项目使用的是老版本脚手架生成,Webpack版本是3.x
。分析后,在不更新脚手架的前提下,直接升级Webpack,可以使用splitChunk
进行代码分隔,配合新版本的React的lazy & Suspense
,可不引入三方库的情况下实现按需加载。
升级Webpack比较容易,直接升级最新版的react-scripts
即可(注意不是react-script
)。1
yarn add --exact react-scripts@2.1.1
安装完后yarn start
运行项目,不出意外会有报错提示,因为某些react-scripts
依赖的库版本过低,此时只要删除yarn.lock
和package-lock.json
和node_modules
,并且运行yarn install
重新安装依赖即可。
安装完成后,不出意外项目可以正常运行了,此时Webpack已升级。
代码分隔
考虑到初次构建的时候仅仅分离部分第三方库导致第三方包和业务代码之间存在代码冗余,在新构建的版本中,需要对公用代码进行抽取,这样可以直接避免冗余。具体的
splitChunk
配置这里不再列出,抽取完成后例如lodash
、moment
这样的公共库被抽取到了公共包中,构建完成后生成了三个包:业务代码及未抽取的第三方包、特定的第三方包、公共代码包,构建体积明显减少。由于业务的特殊性,首页并不包含任何图表,因此不需要加载图表库,并且图表库打包完成后大小为
700KB
(未Gzip),因此考虑使用基于路由的按需加载来去除首页不需要加载的资源。进行代码分隔依赖于运行时加载,也就是
import()
,这是一个ES
提案,目前还未标准化(babel已实现)。不同于import
语法在预编译时链接代码,import()
可以在运行时异步加载资源。而Webpack在解析到这种语法的时候,就会自动的进行代码分隔。目前React懒加载的方案有很多,比如
react-loadable
,但由于新版的React已经支持了代码分隔,就没必要引入额外的库。React.lazy
函数能够让你像处理常规组件一样处理懒加载组件。该函数接收一个函数作为参数,这个函数必须使用到import()
动态加载组件,import()
返回一个Promise
,在resolve
后返回这个React组件。Suspense
是一个组件,其主要的作用是在懒加载块没有加载完成时使用占位组件进行UI 的优雅降级。经过分析,基于路由的分隔可能最适合本项目。涉及到图表库的三个页面使用懒加载进行代码分隔,其余页面打包在一起,并且完全分离第三方库和业务代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14import { lazy, Suspense } from 'react';
import { render } from 'react-dom';
import Loading from '@/components/loading'
const ChartPage = lazy(() => import(/* webpackChunkName: "detail" */'@/views/chartPage'));
const App = () => (
<Router>
<Suspense fallback={Loading}>
<Switch>
<Route path='/detail' component={ChartPage} />
...
</Switch>
</Suspense>
</Router>
);
直接运行yarn start
,观察打包结果,发现已经按照路由进行了分隔,在总打包大小基本不变的情况下,初始加载的包只有230KB
,减少了一大半。
- 观察打包结果,发现公共包的命名是
0.hash.chunk.js
,对于已经在注释中指定了webpackChunkName
,这显然是不正常的,可能是react-scripts
默认分隔策略的问题。为了修复这个命名问题并且进一步分隔runtime chunk
,对splitChunk
进行少量的配置。1
2
3
4
5
6
7
8
9
10const SplitCodeConfig = {
optimization: {
splitChunks: {
chunks: 'all',
},
runtimeChunk: {
name: 'runtime'
}
}
}
只需要进行简单的配置,设置splitChunks
的chunks
为all
, Webpack就会使用默认的规则进行打包:抽取公共包,抽取node_modules
第三方包等等,具体见Webpack官网 -> 文档 -> 代码分离。
至此,项目构建已经完成,构建的包可以直接用于生产换环境。
思考
在项目中使用到的组件库包含近百个通用组件和业务组件,但是项目中只使用到了十余个,并且通过观察打包结果,可以看到未被使用到的组件也被打包进来了,这显然不合理。针对这个问题,我们可以查看对应组件库的按需加载方案,并且在Webpack进行相应的配置即可。这里由于使用的是内部开发的组件库,就不在举例。
除了使用的组件库,我们需要找到所有能够按需加载却在项目中没有使用的第三方库。例如我要使用
lodash
的debounce
函数进行防抖,我在项目中是这样使用的:1
2import _ from 'lodash';
window.onscroll = _.debounce(handler);
在构建的时候,整个lodash
会被打包,如果只需要打包debounce
函数,在import
的时候直接import debounce from 'lodash/debounce'
即可。
其它
为了直观的查看构建结果,推荐使用
webpack-bundle-analyzer
插件,它会在构建完成后打开一个页面,该页面可以看到每个bundle
的打包情况。使用也很简单:1
2
3
4
5
6
7const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
{
...
plugins: [
new BundleAnalyzerPlugin(),
]
}如果想测试
Gzip
后的文件大小,在webpack-dev-server
的配置项中添加compress: true
即可。但如果查看生产环境下构建的Gzip
包大小,可以使用compression-webpack-plugin
插件,该插件会在构建完成后生成对应bundle
的Gzip
包。1
2
3
4
5
6
7const CompressionPlugin = require('compression-webpack-plugin');
{
...
plugins: [
new CompressionPlugin({ threshold: 8192 }), // 只有大于8KB的资源才压缩
]
}
在一般的情况下开启Gzip
只需要设置生产服务器即可,并不需要在构建时生成Gzip
包,但如果每次请求时再Gzip
无疑会加大服务器的负担,因此事先准备好gzip
包,服务器不必压缩而是直接返回,这不失为一个好的选择。
开启Gzip
会极大的减小传输体积,但无论是压缩还是解压都需要大量的运算,对于某些较小的资源,使用Gzip
可能反而会降低性能,因此建议只对较大的资源进行Gzip
压缩,而较小的资源直接传输。
完。