在平常的开发过程中,如果没有手动地配置或者优化打包后尺寸,那么用户打开网站时,首屏加载会很慢,几秒后才出现内容,大大增加了用户的等待时间。
为了解决这个问题,我们需要从打包这个环节进行优化。常见的优化打包工具webpack,我们从流行的React和Vue库着手,尝试着优化它们。
首先,使用create-react-app脚手架创建一个React应用。
如果没有,首先需要从全局中安装脚手架,命令如下:
npm install create-react-app -g
接着新建一个文件夹,并在终端编写命令为:
create-react-app webpack-optimiation-react
模板创建完毕之后,它目录结构如下:
.
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── README.md
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── serviceWorker.js
│ └── setupTests.js
├── tree.txt
└── yarn.lock
模板文件建好后,我们在不同的库中添加相同的代码,然后使用router使他们能够正常运转。
// src/Home.js
import React from 'react';
export default () => <h1>Home</>;
// src/About.js
import React from 'react';
export default () => <h1>About</>;
// src/Concat.js
import React from 'react';
export default () => <h1>Concat</h1>;
然后使用npm包管理器,添加react-router-dom。
npm install react-router-dom -D
以上的代码写好后,在src/index.js中添加:
import React, { lazy, Suspense } from 'react';
import { Switch, BrowserRouter as Router, Link, Route } from 'react-router-dom';
const Home = lazy(() => import('./Home'));
const Concat = lazy(() => import('./Concat'));
const About = lazy(() => import('./About'));
const NavBar = () => (
<div>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
<Link to='/concat'>Concat</Link>
</div>
);
function App() {
return (
<Router className='App'>
<>
<NavBar />
<Suspense fallback={<div>loading...</div>}>
<Switch>
<Route path='/' exact component={Home} />
<Route path='/about' component={About} />
<Route path='/concat' component={Concat} />
</Switch>
</Suspense>
</>
</Router>
);
}
export default App;
编写完成之后,使用npm run start,浏览器会自动打开,并显示:

OK,运行正常。
我们打包试试,在终端输入命令:
npm run build
这时就会出现一个build文件夹,这就是打包后的结果,可以给后端部署了。
build之后的文件大小居然有600k之多,为了查看具体那些包体积大,就需要配置webpack。

但是create-react-app并不暴露webpack配置文件,需要输入命令才能看到webapck配置文件:
npm run eject
它的结构目录如下:
.
├── config
│ ├── env.js
│ ├── jest
│ │ ├── cssTransform.js
│ │ └── fileTransform.js
│ ├── modules.js
│ ├── paths.js
│ ├── pnpTs.js
│ ├── webpack.config.js
│ └── webpackDevServer.config.js
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── README.md
├── scripts
│ ├── build.js
│ ├── start.js
│ └── test.js
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── serviceWorker.js
│ └── setupTests.js
├── tree.txt
└── yarn.lock
为了解决上面打包600kb的问题,首先需要分析,那些地方打包后的尺寸过大。我们需要webpack-bundle-analyzer这个插件才能可视化地看到那些包尺寸较大。
安装方法:
npm install webpack-bundle-analyzer -D
然后在config/webpack.config.js写入代码:
首先导入这个包,然后再添加插件:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins:[
...
isEnvProduction && new BundleAnalyzerPlugin(),
// isEnvProduction这个变量是指,在是否在生产环境
...
]
可视化分析出来的结果如下:

从上图我们可以发现,打包后最大的包是react-dom。
那么我们就着手优化它吧,从两个方面考虑:
-
减少服务器端的压力方法
-
减少尺寸的方法
减少服务器压力的方法有:
-
使用
AggressiveSplittingPlugin插件 -
使用
prefetch&preload方法 -
使用
gzip方法
减少尺寸的方法有:
-
使用
ModuleConcatenationPlugin插件 -
使用
uglifyjs插件 -
使用
exterals选项
AggressiveSplittingPlugin
那么现在试着用AggressiveSplittingPlugin优化打包后的代码吧。
直接在config/webpack.config.js的plugins中添加代码:
plugins: [
new webpack.optimize.AggressiveSplittingPlugin({
minSize: 3000,
maxSize: 5000,
chunkOverhead: 0,
entryChunkMultiplicator: 1
})
];
build文件夹中出现了非常多的小文件。这个插件是的超过一定体积会分割文件。有利于减少服务器的请求压力。AggressiveSplittingPlugin可以将bundle拆分成更小的chunk,直到各个chunk的大小达到option设置的 maxSize。它通过目录结构将模块组织在一起。
它记录了在webpack Records里的分离点,并尝试按照它开始的方式还原分离。这确保了在更改应用程序后,旧的分离点(和 chunk)是可再使用的,因为它们可能早已在客户端的缓存中。因此强烈推荐使用Records。
preload&prefetch
第二项是预加载的能力,和预请求能力。我们先来看看使用preload会怎么样?
我的html-webpack-plugin版本是4.0.0-beta.11,为了正确安装这个 preload 插件,必须要将它的版本设置为3.0.0-beta.3,不然它会报以下的错误:
Plugin could not be registered at 'html-webpack-plugin-before-html-processing'. Hook was not found.
使用npm管理器安装这个版本:
npm install preload-webpack-plugin@3.0.0-beta.3
接着在webpack.config.js中添加:
const PreloadWebpackPlugin = require('preload-webpack-plugin');
// ...
plugins: [
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true
}
}
: undefined
)
),
// ...
new PreloadWebpackPlugin()
];
// ...
打开调试台,如果的你节点出现了这样的样子,那么说明你成功了。

后面有一个ref=preload就是预加载。
preload有能做什么?
- 可以预先加载文件或者资源。
- 不会阻塞页面加载。
举个例子,如果一个网页中使用了许多的字体文件,那么用户在浏览的时候就会等待字体加载,出现白屏的现象。
如果使用了preload的话,那么浏览器不会每次请求一个页面,到指定页面中重新加载,而是预先加载字体文件,到指定页面中不用再次加载。
现在,我们尝试使用prefetch,是什么结果。
在config/webpack.config.js修改:
// ...
plugins: [
// ...
new PreloadWebpackPlugin({
rel: 'prefetch'
})
// ...
];
// ...
这时,preload就变成了prefetch,如下图:

如果你看到了ref=prefetch的话,那么代表预处理成功。
它们的共同点是?
- 异步加载资源,不会阻塞网页渲染
- 下载并不执行文件
- 能够提前请求文件
- 没有同域名限制
prefetch 和 preload 的区别是什么?
preload会首先优先加载,并且会占用HTTP并发数,也就是刚进入页面就会请求。而prefetch会浏览器出空闲期时,再请求文件。preload可以跨域请求,prefetch不会。
Gzip
gzip能够为我们减少存储空间,以及减少传输的时间。
我们需要安装一下插件:
npm install -D compression-webpack-plugin
在config/webpack.config.js中添加:
//...
plugins: [
//...
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js|css)/,
threshold: 1024,
minRatio: 0.8
})
//...
];
//...
build完成之后,打开浏览器。如果你看到这样的header的话,那么代表开启gzip成功。

现在,我们开始减少尺寸的优化。
使用ModuleConcatenationPlugin可以提高代码的执行速度和预编译:
在config/webpack.config.js中添加:
// ...
plugins: [
// ...
new webpack.optimize.ModuleConcatenationPlugin()
];
// ...
build后,尺寸比以前集减少接近10kb,而且网页的打开速度也提高不少。
相比之前打开网页的速度,提高了50ms之多。再加上懒加载,并不需要提前加载所有页面,所以首屏渲染速度提高了许多。
Uglify
接着,我们上一个大杀器,UglifyJS插件,它最大程度压缩和丑化代码。
安装它的命令行:
npm install -D uglifyjs-webpack-plugin
导入到config/webpack.config.js中,并使用:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
// ...
optimization: {
minimizer:{
new UglifyJsPlugin(),
}
}
// ...
可以看见,压缩后的代码有多180kb。

Externals
externals这个webpack选项是在打包过程中剔除掉。这样就有效地减少了依赖,使用第三方CDN减少HTTP压力和请求的压力。
使用externals只需要在webpack.config.js中添加一下的代码:
// ...
externals:{
'react': 'React',
'react-dom': 'ReactDOM',
'react-router-dom': 'ReactRouterDOM'
}
// ...
接着在index.html中添加CDN。
<script src="https://cdn.bootcss.com/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdn.bootcss.com/react-router-dom/5.1.2/react-router-dom.min.js"></script>
<script src="https://cdn.bootcss.com/react-dom/16.10.2/umd/react-dom.production.min.js"></script>