[webpack]首屏加载优化的实践

3,393 阅读5分钟

profile 概览

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

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

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

github.com/atbulbs/web…

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

babeljs.io/docs/en/bab…

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' // 生产时的最佳实践