首屏加载优化的webpack实践

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…javascript

bundle analysis 打包分析

官方推荐的一些打包分析插件 webpack.js.org/guides/code…css

安装和使用webpack-bundle-analyzer github.com/webpack-con…html

# 生成stats.json文件
$ webpack --profile --json > stats.json
复制代码

code coverage 代码覆盖率

官方文档: developers.google.com/web/updates…前端

参考文档: blog.logrocket.com/using-the-c…java

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

magic comments 魔法注释

官方文档: webpack.js.org/api/module-…node

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

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

// 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…es6

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

  • 并且若是不分割, 修改业务代码后, 从新访问, 又所有得从新加载库代码

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

    动态引入文档 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' // 生产时的最佳实践
复制代码
相关文章
相关标签/搜索