首屏加载优化的webpack实践

profile 概览

本文总结了一些首屏加载优化的webpack实践, 分为如下几个模块

  • bundle analysis 打包分析
  • code coverage 代码覆盖率
  • magic comments 魔法注释
  • prefeching/preloading 预加载
  • code splitting 代码分割
  • lazy loading 懒加载(按需加载)
  • babel, babel-polyfill与babel-runtime
  • tree shaking 摇树
  • sourcemap 源码映射配置

本文所用demo代码的仓库地址

https://github.com/atbulbs/webpack_optimization

bundle analysis 打包分析

官方推荐的一些打包分析插件 https://webpack.js.org/guides/code-splitting/#bundle-analysis

安装和使用webpack-bundle-analyzer https://github.com/webpack-contrib/webpack-bundle-analyzer

1
2
# 生成stats.json文件
$ webpack --profile --json > stats.json

code coverage 代码覆盖率

官方文档: https://developers.google.com/web/updates/2017/04/devtools-release-notes
参考文档: https://blog.logrocket.com/using-the-chrome-devtools-new-code-coverage-feature-ca96c3dddcaf/

  • 适用于JS和CSS
  • 若某文件代码覆盖率低, 影响首屏时间和性能
  • 解决: 首屏用户交互后才执行的关键代码预加载 + 非关键代码懒加载 + 移除无用代码 tree shaking
1
2
// 查看代码覆盖率
// dev tool > command + shift + P > Show Coverage > 录屏

magic comments 魔法注释

官方文档: https://webpack.js.org/api/module-methods/#magic-comments

1
2
3
4
5
6
7
8

function getComponent () {
return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
})
}

prefeching/preloading 预加载

官方文档: https://webpack.js.org/guides/code-splitting#prefetchingpreloading-modules

实现第一次加载的时候就是最快的, webpack推荐交互的代码放到异步加载的模块里去写
prefeching/preloading可实现网页空闲时预先加载异步模块

1
2
3
4
5
6
7
8
9
10
11
// src/click.js
export default funciton clickHandler () {
console.log('clicked')
}

// src/index.js
window.document.addEventListener('click', () => {
// prefetch会等待核心代码加载完成, 页面空闲时去加载prefetch的文件
// webpackPreload会和核心代码一起加载
import(/* webpackPrefetch: true */'./click.js').then({ default: func } => func())
})

code splitting 代码分割

官方文档 https://webpack.js.org/guides/code-splitting

  • 分割业务代码和库代码, 不然打包文件会很大, 首次访问加载时间会很长

  • 而且如果不分割, 修改业务代码后, 重新访问, 又全部得重新加载库代码

  • 分割方式: 配置 + 同步引入 与 动态引入(无需做任何配置)

    动态引入文档 https://webpack.js.org/guides/code-splitting#dynamic-imports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getComponent () {
// jsonp引入
// 动态的import, 实验性的语法
// npm i babel-plugin-dynamic-import-webpack -D
return import('lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
})
}

getComponent().then(element => {
document.body.appendChild(element)
})

// .babelrc 动态引入
// npm i -D babel-plugin-dynamic-import-webpack
{
plugins: ['dynamic-import-webpack']
}

SplitChunksPlugin官方文档: https://webpack.js.org/plugins/split-chunks-plugin/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//  splitPlugin 配置
optimization: {
// SplitChunksPlugin config
// 如下是官方默认配置
splitChunks: {
// async 只对异步代码生效
// all 对同步异步都做代码分割, 但是同步代码还需cacheGrops配置
// initial 只对同步代码做分割
chunks: 'async',
// 如果引入的模块大于minSize才做代码分割
minSize: 30000,
// 对于大于maxsize的模块尝试进行二次代码分割
maxSize: 0,
// 打包后的文件至少有多少个chunk文件引入这个模块才进行代码分割
minChunks: 1,
// 同时加载的模块数量,
// 在打包前5个库的时候会生成5个js文件,
// 超过5个就不再做代码分割
maxAsyncRequests: 5,
// 入口文件做代码分割的最大文件数量
maxInitialRequests: 3,
// 自动命名定界符
automaticNameDelimiter: '~',
// 让cacheGroups里的filename生效
name: true,
// 缓存组, 把库文件先放到缓存里, 再根据test规则分组合并打包
cacheGroups: {
// vendors: false
vendors: {
// 如果是node_modules里面的文件, 就打包到vendors组里
test: /[\\/]node_modules[\\/]/,
// 分组时的优先级
priority: -10
// // 组文件的名字 vendors.js, 不然会是 vendors~main.js
// filename: 'vendors.js'
},
// 被分割的代码的默认的配置, 没有test, 所有模块都符合要求
default: {
// 至少被引用了2次
minChunks: 2,
priority: -20,
// 复用已被分割打包过了的模块
reuseExistingChunk: true,
// // 组的文件名
// filename: 'common.js',
}
}
}
}

lazy loading 懒加载(按需加载)

官方文档: https://webpack.js.org/guides/lazy-loading
前端框架结合webpack实现懒加载: https://webpack.js.org/guides/lazy-loading#frameworks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 点击页面才会加载lodash代码
function getComponent () {
// 懒加载并不是webpack里面的一个概念, 而是ES的import语法,
// webpack能识别这种语法, 对import引入的模块做代码分割
// 相当于对optimization的splitChunks做了配置
return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
})
}

window.document.addEventListener('click', () => {
getComponent().then(el => window.document.body.appendChild(el))
})

babel, babel-polyfill与babel-runtime

对比:

  • babel只转换语法, 不转换兼容api, 很多模块中很可能有重复helper函数
  • babel-polyfill在全局做兼容, 会污染全局变量, 不建议在类库中使用
  • babel-runtime属于babel的包, 也可提供polyfill, 且是默认按需加载的, helper函数会作为公共的模块使用, 缺点: 业务代码中的实例方法会失效
  • 使用了babel-plugin-transform-runtime, babel就会启用bable-runtime

总结:

  • 业务代码使用 babel-polyfill + 按需引入
  • 类库代码使用 babel-runtime

最佳实践

  • 业务代码可使用 useBuiltIns 配置, 会自动按需引入babel-polyfill, 无需手动引入babel-polyfill

https://babeljs.io/docs/en/babel-preset-stage-0#usebuiltins

tree shaking 根据引入的按需打包, 摇晃掉模块里与树没有关联的无用的模块

官方文档: https://webpack.js.org/guides/tree-shaking
官方文档: https://webpack.js.org/configuration/optimization#optimizationusedexports
关于静态模块结构的官方文档: https://exploringjs.com/es6/ch_modules.html#static-module-structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Tree Shaking只支持 ES Module(静态引入), 不支持Common JS(动态引入)
// development mode 默认没有tree shaking
// production mode 不需要这个optimization
optimization: {
usedExports: true
}

// package.json
"sideEffects": false, // false时对所有模块摇树
// 实践
"sideEffects": [
// 不然打包时会忽略 @babel/polly-fill, 因为其没有导出对象, 只在window上挂载了对象
"@babel/polly-fill",
// 对css不摇树
"*.css"
],

sourcemap 源码映射

官方文档: https://webpack.js.org/configuration/devtool#devtool

1
2
3
4
5
6
7
8
devtool: 'none' // 关闭sourcemap
devtool: 'source-map' // 会生成一个.map文件
devtool: 'inline-source-map' // .map文件会被打包到js文件里, 错误提示会精确到第几行第几列
devtool: 'cheap-inline-source-map' // 只精确到行, 不精确到列, 提示性能, 而且只会提示业务代码的错误, 不提示loader和第三方模块的错误
devtool: 'cheap-module-inline-source-map' // 提示loader和第三方模块的错误
devtool: 'eval' // 用js eval效率最高, 提示不全面, 不展示行数
devtool: 'cheap-module-eval-source-map' // 开发时的最佳实践
devtool: 'cheap-module-source-map' // 生产时的最佳实践