profile 概览
本文总结了一些首屏加载优化的webpack实践, 分为如下几个模块
- bundle analysis 打包分析
- code coverage 代码覆盖率
- magic comments 魔法注释
- prefeching/preloading 预加载
- code splitting 代码分割
- lazy loading 懒加载(按需加载)
- babel, babel-polyfill与babel-runtime
- tree shaking 摇树, 或者叫剪枝
- sourcemap 源码映射配置
本文所用demo代码的仓库地址
bundle analysis 打包分析
官方推荐的一些打包分析插件 webpack.js.org/guides/code…
安装和使用webpack-bundle-analyzer github.com/webpack-con…
# 生成stats.json文件
$ webpack --profile --json > stats.json
code coverage 代码覆盖率
官方文档: developers.google.com/web/updates…
参考文档: blog.logrocket.com/using-the-c…
- 适用于JS和CSS
- 若某文件代码覆盖率低, 影响首屏时间和性能
- 解决: 首屏用户交互后才执行的关键代码预加载 + 非关键代码懒加载 + 移除无用代码 tree shaking
// 查看代码覆盖率
// dev tool > command + shift + P > Show Coverage > 录屏
magic comments 魔法注释
官方文档: webpack.js.org/api/module-…
function getComponent () {
return import(/* webpackChunckName: 'lodash' */'lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['a', 'b'], '*')
return element
})
}
prefeching/preloading 预加载
官方文档: webpack.js.org/guides/code…
实现第一次加载的时候就是最快的, webpack推荐交互的代码放到异步加载的模块里去写 prefeching/preloading可实现网页空闲时预先加载异步模块
// 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 代码分割
官方文档 webpack.js.org/guides/code…
-
分割业务代码和库代码, 不然打包文件会很大, 首次访问加载时间会很长
-
而且如果不分割, 修改业务代码后, 重新访问, 又全部得重新加载库代码
-
分割方式: 配置 + 同步引入 与 动态引入(无需做任何配置)
动态引入文档 webpack.js.org/guides/code…
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官方文档: webpack.js.org/plugins/spl…
// 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 懒加载(按需加载)
官方文档: webpack.js.org/guides/lazy…
前端框架结合webpack实现懒加载: webpack.js.org/guides/lazy…
// 点击页面才会加载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
tree shaking 根据引入的按需打包, 摇晃, 或者叫剪枝掉模块里与树没有关联的无用的模块
官方文档: webpack.js.org/guides/tree…
官方文档: webpack.js.org/configurati…
关于静态模块结构的官方文档: exploringjs.com/es6/ch_modu…
// 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 源码映射
官方文档: webpack.js.org/configurati…
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' // 生产时的最佳实践