likes
comments
collection
share

webpack优化方案 | 实践总结

作者站长头像
站长
· 阅读数 41

本文 demo 地址

本文项目代码位置: 源码地址

为什么需要构建工具

webpack 能干什么?

  • 开发时,启动本地服务
  • 解决 js、css 的依赖问题。(以前经常因为引入顺序问题,导致 css 没起作用或某个js变量找不到)
  • 将 ES6、vue/react、jsx 语法编译为浏览器可识别的代码
  • 合并、压缩、优化打包后的体积
  • css 前缀补齐/预处理器
  • 使用 eslint 校验代码
  • 单元测试

webpack 配置组成部分

module.exports = {
  entry: '',   // 指定入口文件
  output: '',  // 指定输出目录和输出文件名
  mode: '',    // 环境
  module: {
    rules: [   // loader配置
      {test: '', use: ''}
    ]
  },
  plugins: [   // 插件配置
    new xxxPlugin()
  ]
}

基础常用的 Loader

为什么需要 loader ?

webpack 原生只支持 js、json 两种模块类型,所以需要 loader 把其他类型的文件转化成有效的模块,并可以添加到依赖图中。

loader 本身是一个函数,接受源文件作为参数,返回转换的结果

功能loader说明
解析es6babel-loader配合.babelrc使用
解析vuevue-loader
解析csscss-loader用于加载.css文件,并转换成commonjs对象
style-loader将样式通过<style>标签插入到head中
解析lessless-loader将less转换成css
解析图片和字体file-loader用于处理文件(图片、字体)
url-loader也可以处理图片和字体,和file-loader功能类似,但它还可以设置较小资源自动转base64(内部用了file-loader),使用options:{limit: xxx}

基础常用的 Plugin

插件用于 bundle 文件的优化、资源管理和环境变量注入,它作用于整个构建过程

  • clean-webpack-plugin: 打包前自动清理 dist 目录
  • html-webpack-plugin:自动生成 html,并将打包后的 js 插入进去
  • mini-css-extract-plugin: 把 css 提取成单独的文件,与 style-loader 功能互斥,不能同时使用
  • 关于代码压缩: JS文件的压缩 :默认开启了内置的 terser-webpack-plugin,webpack 在打包时会自动压缩 js 代码 css文件的压缩 :使用 optimize-css-assets-webpack-plugin ,同时使用 cssnano(处理器)
 plugins: [
   new OptimizeCssAssetsPlugin({
     assetNameRegExp: /\.css$/g,
     cssProcessor: require('cssnano')
   })
 ]

html文件的压缩:修改 html-webpack-plugin,设置压缩参数

plugins: [
  new HtmlWebpackPlugin({
    template: path.join(__dirname, 'src/index.html'),
    filename: 'index.html',
    chunks: ['main', 'other'], //要包含哪些chunk
    inject: true, //将chunks自动注入html
    minify: { // 压缩相关
      html5: true,
      collapseWhitespace: true, //压缩空白字符
      preserveLineBreaks: false,
      minifyCSS: true,
      minifyJS: true,
      removeComments: true
    }
  })
]

webpack性能优化

开发环境性能优化

模块热替换(HotMoudleReplacement)

开启 dev-server 后默认只要有一个文件变化,会重新构建,刷新浏览器页面。

模块热替换: 只重新打包变更的模块,局部刷新,保留数据状态,(而不是将所有模块重新打包,刷新页面)提升构建速度,使开发更加方便

通过 devServer.hot 启用,其内部依赖 webpack.HotModuleReplacementPlugin 实现,HotModuleReplacementPlugin 会在 hot: true时自动被引入,可以不写

  • 样式文件:可以直接使用 HMR 功能,因为 style-loader 内部实现了
  • js 文件:默认不能使用 HMR 功能,需要修改 js 代码,添加支持 HMR 功能的代码
if(module.hot){  // 如果开启了HMR功能
	// 监听xxx.js文件的变化,一旦发生变化,其他模块不会重新打包,会执行回调函数
	module.hot.accept('./xxx.js', function(){ 
    	fn()
    })
}
  • html文件:没有热替换,也没有热更新,热更新可以通过在入口文件添加 html 文件路径来打开,但通常没有必要

使用source-map

由于经过 webpack 打包后的代码是经过各种 loadersplugins 转换过后的一个大的js文件,开发过程中无法调试。source-map 是一种提供源代码到构建后代码映射的技术,报错时通过 source map 可以定位到源代码。 启用方式:

module.exports = {
  devtool: 'source-map' 
}

选项: [inline- | hidden- | eval-] [nosources- ] [cheap- [module- ]]source-map

  • source-map: 产生.map 文件,提供错误代码准确信息和源代码的错误位置
  • inline:内联,将.map 作为 DataURI 嵌入,不单独生成 .map 文件,构建速度更快
  • eval:内联,使用 eval 包裹模块代码,指定模块对应文件
  • cheap:只精确到行,不精确到列
  • module:包含 loadersourcemap

推荐组合:

  • 开发环境:速度快,调试更友好 eval-source-map (eval 速度最快,source-map 调试最友好)

  • 生产环境:内联会让代码体积变大,所以生产环境不用内联 1.考虑是否要隐藏源代码? nosources-source-map ---全部隐藏 hidden-source-map ---只隐藏源代码,会提示构建后代码错误信息 2.考虑是否要调试友好? source-map

生产环境性能优化

使用文件指纹进行版本控制和缓存

当设置了 http 强缓存,比如有效期为一天,如果不使用 hash,当这个文件改变了,因为文件名没变,所以客户端使用的还是旧的缓存;如果使用了 hash,这时文件名就改变了,就会请求新的资源,而没有更改过的文件继续使用缓存

  • hash: 构建的 hash,每次构建都会改变,不建议使用
  • chunkhash: 和 webpack 打包的 chunk有关,不同的 entry 会生成不同的 chunkhash
  • contenthash: 根据文件内容来定义hash,文件内容不变,则 contenthash 不变,推荐在 css 文件上使用

js 文件的指纹设置:

//设置 output 的 filename,使用 [chunkhash]
module.exports = {
  output: {
    filename: '[name][chunkhash:8].js',
    path:__dirname+'/dist'
  }
}

css 文件的指纹设置:

使用 MiniCssExtractPlugin cssjs 中提出来,然后使用 [contenthash]

plugins: [
  new MiniCssExtractPlugin({
    filename: '[name][contenthash:8].css'
  })
]

补充: module、chunk、bundle的区别

  • module:模块,源代码中的一个文件就是一个模块
  • chunk:一个入口文件所依赖的一大块就是一个 chunk,可以理解为一个 entry 对应一个 chunk
  • bundle:打包后的资源,一般来说一个 chunk 就对应一个 bundle,但也可以通过一些插件进行拆包,把一个大chunk 拆分为多个 bundle,比如 MiniCssExtractPlugin

tree shaking

tree shaking(摇树优化):一个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打到 bundle 里面去,tree shaking 就是只把用到的方法打入bundle,没用到的方法会在 uglify 阶段被擦除掉。

使用:webpack 默认支持,在 .babelrc里设置 module:false 即可

webpack 会在 production mode 的情况下默认开启 tree shaking 要求:必须是 es6 语法,cjs 的方式不支持

tree shaking 原理

DCE:永远不会被用到的代码,比如引入了一个方法但是没调用 或者 if(false){xxx}

利用 ES6 模块的特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import bindingimmutable

在打包之前静态的分析文件,在uglify阶段删除无用代码

code split

将一个大bundle文件拆包,拆包的方案可以在cacheGroups里配置

  • splitChunks
// splitChunks默认配置
optimization: {
    splitChunks: {
      chunks: 'all',  // 无论同步引入还是异步引入
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,  // 匹配node_modules目录下的文件
          priority: -10   // 优先级配置项
        },
        default: {
          minChunks: 2,  // 至少引用了2次
          priority: -20,   // 优先级配置项
          reuseExistingChunk: true
        }
      }
    }
  }

在默认设置中

  • 会将 node_mudules 文件夹中的模块打包进一个叫 vendorsbundle
  • 所有引用超过两次的模块分配到 default bundle 中 ,可以通过 priority 来设置优先级。

DLL

将第三方库和业务基础包单独打成一个文件,只在第一次打,或者需要更新依赖的时候打,此后每次就可以只打自己的源代码,加快了构建速度。

方法:使用 DLLPlugin 进行分包,DllReferencePluginmanifest.json 引用

分包需要单独的配置文件:

// webpack.dll.js
module.exports={
  entry: {
    lib: [
      'lodash',
      'jquery'
    ]
  },
  output: {
    filename: '[name]_[chunkhash].dll.js',
    path: path.join(__dirname, 'build/lib'),
    library: '[name]' // 打包后对外暴露的全局变量名称
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]', // manifest.json中的name,要与ouput.library名一致
      path: path.join(__dirname, 'build/lib/manifest.json'),
    })
  ]
}

package.json 中添加命令对 dll 单独打包:

"scripts": {
    "dll": "webpack --config webpack.dll.js"
  },

使用 DllReferencePluginmanifest.json 进行引用,告诉 webpack 使用了哪些动态链接库,不用再打包这里面的东西

// webpack.prod.js
new webpack.DllReferencePlugin({
  manifest: require('./build/lib/manifest.json')
}),

使用 addAssetHtmlWebpackPlugindll 资源插到 html

// webpack.prod.js
new addAssetHtmlWebpackPlugin([
  {
    filepath: path.resolve(__dirname, './build/lib/*.dll.js'),
    outputPath: 'static', // 将*.dll.js拷贝后的输出路径,相对于html文件
    publicPath: 'static' 
  }
])

webpack优化方案 | 实践总结

demo 地址: webpack实践-dll-plugin分支

使用前后对比:

使用 dllplugin 前,基础库打到了 main.js 里,占了 160kbwebpack优化方案 | 实践总结 使用后:main.js 只剩 1.23kb webpack优化方案 | 实践总结

splitChunks 和 dll 的区别

  • splitChunks 是在构建时拆包,dll 是提前构建好基础库,打包的时候就不需要打基础库了,时间上 dllsplitChunks 快一点
  • dll 需要多配置一个 webpack.dll.config.js ,而且一旦 dll 中的依赖有更新,得走两遍打包,比 splitChunks 麻烦一些
  • 推荐使用 splitChunks 去提取页面间的公共 js 文件。DllPlugin 用于基础包(框架包、业务包)的分离。

多进程打包

使用 thread-loader 开启多进程打包,加快打包速度! 注意:启动进程需要大概 600ms ,进程间通信也有花销,项目小的话开启多进程得不偿失,所以只有当项目比较大,打包耗时较长的时候才适合使用多进程。

module: {
  rules: [
    {
      test: /.js$/, 
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: 2 //开启两个进程
          }
        },
        {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            cacheDirectory: true 
          }     
        }
      ]
    },
  ]
}

多页面打包通用配置

多页面打包需要多个入口文件,多个 HtmlWebpackPlugin 产生多个 html。我们不可能去手写很多个入口和 HtmlWebpackPlugin

方案: 动态获取 entry 和设置 html-webpack-plugin 数量

// 核心方法
const setMPA = () => {
  const entry = {};
  const htmlWebpackPlugins = [];

  const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
  entryFiles.forEach(entryFile => {
    const match = entryFile.match(/src\/(.*)\/index\.js/);
    const pageName = match && match[1];

    entry[pageName] = entryFile;
    htmlWebpackPlugins.push(
      new HtmlWebpackPlugin({ 
        template: path.join(__dirname, `src/${pageName}/index.html`),
        filename: `${pageName}.html`,
        chunks: [pageName], //要包含哪些chunk
        inject: true, //将chunks自动注入html
        minify: {
          html5: true,
          collapseWhitespace: true,
          preserveLineBreaks: false,
          minifyCSS: true,
          minifyJS: true,
          removeComments: false
        }
      }),
    )
  })

  return {
    entry,
    htmlWebpackPlugins
  }
}

其他具体配置见 webpack实践-mpa-build分支

转载自:https://juejin.cn/post/6880487034130169869
评论
请登录