之前一直讲的都是提升构建性能,那么如何提高生产环境下的访问性能呢?例如首页白屏,文件过大导致加载时间过长,文件太多细碎导致
http通讯次数过多降低性能,http缓存失效导致资源重新拉取等问题如何解决。
动态加载
动态加载也就是懒加载,这个在Vue、React等SPA应用下面是很常见的优化技巧,例如Vue通过路由实现页面按需加载,那这个在webbpack中是怎么实现的呢?
由于我之前跳过了
splitChunks的章节,这一部分会包含里面的内容,splitChunks我花了很多时间学习,还没完全弄明白,后面会多花一些时间单独写,下面也会把这一块的知识简单的讲一下,为后面的做铺垫。
这里就有一个概念,叫做Async Chunk,他的意思就是异步引用(使用import('./xxx.js')或者require.ensure('./xxx.js'))的模块会单独打包成一个文件,在Vue的Route文件中我们通常都是使用import('./xxx.vue')来定义我们的component,如下
import { createRouter } from "vue-router"; const router = createRouter({ route: [ { path: '/', component: () => import('./Home.vue'), // 这里的 home 就会打包成一个单独的文件 } ] }); export default router;
其实这是webpack赋予的能力,在webpack中,一般情况下,entry定义的入口文件打包之后只会生成一个文件,如果使用到了import('./xxx.js')或者require.ensure('./xxx.js'),那么xxx.js就会单独生成一个文件,来看示例代码:
// webpack.config.js const path = require('path'); module.exports = { mode: "development", entry: { main: './src/main', }, output: { path: path.resolve(__dirname, './dist'), filename: "[name].js", }, } // main.js import './common/common-sync'; // common-sync.js console.log('common-sync')
生成的文件内容如下:
如果common-sync.js的内容过于庞大,又或者main.js直接使用import './xxx.js'同步引用的文件过多,同样也会造成最后生成的文件内容过大,这个时候如果将import './xxx.js'改成import('./xxx.js'),打包后的文件又会发生什么改变呢?来看看:
// main.js 修改 import('./common/common-sync');
构建之后的结果如下:
我们利用这一特性就可以实现按需加载,具体实现需要看业务,其实这个和懒加载图片是一个道理,在用户不使用的时候就不管他,在用户使用的时候就给用户,其实这个用Vue的路由配置来讲就很容易理解了,这里我就简单的模拟一下:
- 配置文件先改一下,方便看效果,依赖和配置什么的之前都讲过,这里温习一下
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: "development", entry: { main: './src/main', }, output: { path: path.resolve(__dirname, './dist'), filename: "[name].js", }, plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html" }) ], devServer: { hot: true, open: true } }
main.js内容如下
// Home要一进页面就看到,所以使用同步的方式加载,会和 main.js 打包到一起 import Home from "./Home"; Home(); document.getElementById('Home').addEventListener('click', () => { Home(); }) // Bar 和 Foo 使用异步的方式加载,会生成两个文件 document.getElementById('Bar').addEventListener('click', () => { // 这里是异步加载,只有点击对应的按钮才会请求资源 import('./Bar').then(bar => { bar.default() }); }) document.getElementById('Foo').addEventListener('click', () => { import('./Foo').then(bar => { bar.default() }); })
index.htm页面会有对应的dom信息,如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <button id="Home">Home</button> <button id="Bar">Bar</button> <button id="Foo">Foo</button> <div id="page"></div> </body> </html>
Bar.js和Foo.js如下:
function createPage() { const page = document.getElementById('page'); page.innerHTML = '<h1>Bar...</h1>'; // Foo.js就是文字不一样,就不贴代码了 } export default createPage;
上面的配置完了之后,可以执行npx webpack查看构建出来的文件,也可以使用npx webpack server来查看对应的页面效果,查看页面效果记得打开调试模式查看网络信息。
HTTP 缓存优化
还是接着上面的例子,但是需要修改一下配置,主要是修改output.filename,完整配置如下:
const path = require('path'); module.exports = { mode: "development", entry: { main: './src/main', }, output: { path: path.resolve(__dirname, './dist'), filename: "[name]-[contenthash].js", } }
先来说一下这个配置的意思,当然还是要参考官网output.filename,我这里就简单的复制一下课程中的解释:
[fullhash]:整个项目的内容 Hash 值,项目中任意模块变化都会产生新的fullhash;[chunkhash]:产物对应 Chunk 的 Hash,Chunk 中任意模块变化都会产生新的chunkhash;[contenthash]:产物内容 Hash 值,仅当产物内容发生变化时才会产生新的contenthash,因此实用性较高。
然后我上面就使用[name]-[contenthash],这样只有文件内容发生改变,或者文件名发生改变,生成的资源文件名才会发生变化,现在npx webpack看一下生成的产物,然后再随便修改一下Bar.js或者Foo.js再来看一下生成的产物。
- 第一次
- 第二次
结果是两次生成的文件,生成的资源对应main.js的那个文件会发生改变,明明main.js里面的东西是没有改过的(如果修改Home.js,main.js是一定会变的),这是因为Bar.js产物的引用名称发生变化了。
就是Bar.js的产物名称发生变化,对应的main.js引用的名称也要改,所以main.js的产物名称也就变了,其实main.js并没有改。
这样就导致HTTP缓存失效,因为名称改了,浏览器觉得是新的东西,就又重新加载了。
这里就又引出了一个概念叫做 运行时 ,运行时就是webpack为了保证生成的产物能正常运行,就在生成的代码里面注入了一些代码,例如你在代码里面写了require,你在浏览器里面敲敲看,这肯定是没有的呀,这就是webpack给你提供的。
这里webpack提供了一个配置可以将该代码单独剥离出来,形成一个文件,如下:
module.exports = { // 省略其他配置 optimization: { runtimeChunk: 'single', } }
使用外置依赖
首先要弄明白什么是外置依赖,外置依赖就是项目以外的依赖文件,不归webpack管的依赖文件,就比如我有一个依赖包,他要走cdn加速,我打到代码里面去就不能使用cdn了,这个时候就可以使用外置依赖了(我可能说的不好,具体可以查看官网的解释:externals),webpack中使用externals来配置,如下:
module.exports = { // 省略其他配置 optimization: { runtimeChunk: 'single', } }
这个配置的实际作用目前我还没弄清楚,我能想到的就是我上面举的例子,或者说提升构建构建性能,利用
http缓存机制,第三方库不管构建几次因为没有改动就可以一直有缓存。
使用 Tree-Shaking 删除多余模块导出
Tree-Shaking老生常谈了,中文翻译就是树摇,解释一下就是把树摇一下,把上面没用的枯树叶,或者其他没动的东西摇下来;在代码中的意思就是把用不上的模块,用不上的代码给干掉。
在 Webpack 中,启动 Tree Shaking 功能必须同时满足两个条件:
- 配置
optimization.usedExports为true,标记模块导入导出列表; - 启动代码优化功能,可以通过如下方式实现:
- 配置
mode = production - 配置
optimization.minimize = true - 提供
optimization.minimizer数组
简单来说就是下面这样配置就ok了:
module.exports = { // 省略其他配置 mode: "production", optimization: { usedExports: true, } }
还是使用上面的例子,直接在Home.js中添加多个导出语句,然后在构建一下项目看看结果吧:
function createPage() { const page = document.getElementById('page'); page.innerHTML = '<h1>Home...</h1>' } export function func1() { console.log('func1') } export function func2() { console.log('func2') } export function func3() { console.log('func3') } export default createPage;
使用 Scope Hoisting 合并模块
合并模块是指在默认情况下,webpack会将每次引用都包装成一个函数,引用多少个模块就包装多少个,这无疑是增加了很多代码量,还是拿之前的例子来做测试,将webpack配置的mode属性值先改成development,然后修改一下main.js里面的代码如下:
import Home from "./Home"; Home(); import Bar from "./Bar"; Bar();
接着就是构建看效果,一个模块对应着一个包装函数:
这里的模块合并方法也很简单,Webpack 提供了三种开启 Scope Hoisting 的方法:
- 使用
mode = 'production'开启生产模式; - 使用
optimization.concatenateModules配置项; - 直接使用
ModuleConcatenationPlugin插件。
方法1:
const path = require('path'); module.exports = { mode: "production", entry: { main: './src/main', }, output: { path: path.resolve(__dirname, './dist'), filename: "[name]-[contenthash].js", }, }
方法2:
const path = require('path'); module.exports = { mode: "development", entry: { main: './src/main', }, output: { path: path.resolve(__dirname, './dist'), filename: "[name]-[contenthash].js", }, optimization: { concatenateModules: true, } }
方法3:
const path = require('path'); const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); module.exports = { mode: "development", entry: { main: './src/main', }, output: { path: path.resolve(__dirname, './dist'), filename: "[name]-[contenthash].js", }, plugins: [ new ModuleConcatenationPlugin() ] }
上面三种方式我都测试过有效,但是也是有一些限制的:
- 必须是
ESM模块,原因就是其他,例如AMD、CMD导入导出的内容具有动态性,无法保证会不会出问题。 - 模块被其他模块引用,这是为了防止重复代码,避免A模块有使用打一次,B模块也有使用再打一次。
监控产物体积
在构建的时候,控制台都会输出产物的大小,如果太大了就会抛出警告,但是并不会影响构建。
这一块的监控也是可以配置的:
module.exports = { // ... performance: { // 设置所有产物体积阈值 maxAssetSize: 172 * 1024, // 设置 entry 产物体积阈值 maxEntrypointSize: 244 * 1024, // 报错方式,支持 `error` | `warning` | false hints: "error", // 过滤需要监控的文件类型 assetFilter: function (assetFilename) { return assetFilename.endsWith(".js"); }, }, };
上面的配置表示产物体积如果超过172KB则会报错,这里只是监控,报错并不会影响构建。
如果出现提示了,可以根据上面的方案进行优化处理。
总结
优化也要适可而止,适合自己的优化才是最好的优化,例如分包是优化,包的合并也是优化,但是这两种方案就原理是互斥的,所以要选择最合适的,过度优化可能会带来反效果。






