webpack构建优化清单

最近给项目进行webpack优化,尝试过几乎所有方法,一共26条,列举在此。

优化webpack,首先明确优化目标:

  1. 构建速度: 开发环境项目的启动速度,以及生产环境项目的打包速度。构建时间越短,速度越快越好。
  2. 增量构建速度:开发项目时,每修改一次代码,webpack会对修改的部分进重新构建。增量构建时间越短,速度越快越好。
  3. 打包体积:开发环境webpack打包后,生成的文件体积。打包体积越小越好
  4. 加载体积: 除关注打包体积外,浏览器打开页面,加载资源的体积也很重要。按需加载、缓存等技术可以减少加载体积。加载体积越小越好。

减少依赖包,缩小打包体积,构建速度会更快。使用压缩,打包体积会减小,但构建速度则变慢。讨论webpack的优化,速度体积需同时考虑,二者有时正相关,有时负相关,需要均衡速度和体积。

下文中每个优化建议,都会列出优化目标,影响明显的会加粗。除速度和体积,还有: 分析, 即帮助分析webpack打包性能,呈现打包时间等信息;错误追踪,webpack打包后的文件与源文件不同,需要特殊的处理以方便错误定位和调试。

webpack 在生产环境更关注打包体积,而开发环境更关注打包时间,生产环境和开发环境使用不同的策略,下文每一个优化建议都列出使用环境

webpack及相关打包工具在不断跟新优化中,不同版本可能会有很大不同,请以官方文档为准。本文以webpack4.42为准,与webpack3和webpack5区别较大的地方会标注版本变动

1. 量化打包速度

环境:开发,生产

目标:分析,构建速度,增量构建速度

对webpack进行优化,首先要测量webpack打包耗时,发现问题所在。

  1. 使用 speed-measure-webpack-plugin ,快速测量各插件和loader的耗时。
speed.png

speed-measure-webpack-plugin使用非常简单,直接包裹webpack配置即可

    // npm i -D speed-measure-webpack-plugin

    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
    const smp = new SpeedMeasurePlugin()

    const webpackConfig = smp.wrap({
      // ... webpack config 
    })

speed-measure-webpack-plugin对webpack 4的兼容性不好,一些插件可能会发生错误,需暂停发生错误的插件。

  1. 使用 profilingPlugin , 创建构建性能报告。 profilingPlugin 可以生成一份JSON文件,上传到chrome devtool的performence面板, 可以看到详细的编译过程,寻找性能瓶颈:

    const webpack = require('webpack')
    
    module.exports = {
       //...
         plugins: [
                new webpack.debug.ProfilingPlugin(
            {
                        // 产生出的文件位置,相对于根目录
                        outputPath: 'stats/profileEvents.json' 
                    })
        ] 
    }
    
    

    上传到chrome,出现漂亮的时间线,可供分析:

    perfomrance.png

webpack多进程运行时,插件和loader的运行时间较难观察,比如各插件的运行时间相加会大于总时间,loader的先后顺序可能不对。使用单进程可以保证有效的测试数据。测速时请将 parallelism 配置项设为 false (默认为 true ),确保webpack单线程运行:

module.exports = {
   //...
   parallelism: false
}

除调试webpack外,其他情况请开启多进程。

2. 量化打包体积

环境:开发,生产

目标:分析,打包体积,加载体积

可以通过 webpack-bundle-analyzer 查看打包后的体积。探究某个依赖是否过大,是否存在重复的依赖,是否有功能类似的依赖,是否进行有效的代码分离。

// npm i -D webpack-bundle-analyzer

const BundleAnalyzerPlugin = 
    require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
   //...
     plugins: [
            new BundleAnalyzerPlugin({
            defaultSizes: 'gzip' // 设置默认尺寸为'gzip'
        })
    )
    ] 
}

chunck.png

webpack-bundle-analyzer 显示的尺寸有三种:

  • stat: 未经Uglify、Terser 等插件最小化的体积。最小化js就是我们常说的压缩js,最小化(minify)是指去除注释、简化变量名等操作压缩体积,而压缩是通过gzip等算法压缩体积。
  • parsed: 经Uglify、Terser 等插件最小化后的体积。
  • gzip: 经过gzip压缩后的尺寸。

线上一般按照 gzip 进行数据传输,所以选择 gzip 作为衡量体积的标准。

3. 查看打包详情

环境:开发,生产

目标:分析,构建速度,增量构建速度,打包体积,加载体积

除查看打包体积和时间,有时需要更多打包信息,比如模块的依赖关系,可以用如下方式:

  1. 配置 profile 选项,生成详细打包报告。配置 profile 选项后,每次打包会生成一个json文件,里面有打包的详细信息,也可通过 stats-webpack-plugin 进行更多设置:

    // npm i -D stats-webpack-plugin
    
    const StatsPlugin = require('stats-webpack-plugin')
    
    module.exports = {
      //...
      profile: true // 简单的配置,将在打包文件输出报告
        pulgins: [    // 更灵活的配置
            new StatsPlugin(
            '../../stats/stats.json', {
            exclude: [/node_modules/], 
        })
        ] 
    }
    
    

    将产生的json文件上传至https://webpack.github.io/analyse/ 官方分析工具,查看可读的报告:

stat.png
  1. 打包后,在terminal终端中显示打包详情。可以通过 stats 选项配置terminal中的输出结果,如:

    module.exports = {
      //...
      stats: 'verbose'  // 将在terminal中呈现全部输出 
    };
    
    

    每次打包后,在终端会显示:

budgest2.png
`stats` 的可选址值有:

*   `none`: 没有输出
*   `errors-only` :只显示错误信息,**使用此选项,可提高webpack编译速度**
*   `minimal`: 显示错误信息,或者新的输出
*   `normal` : 标准输出
*   `verbose` : 全部输出,建议用 `profile` 选项输出成文档。

可以个性化配置输出信息。使用Node.js API 时此配置无效,使用 `webpack-dev-server` 时,请将此选项需放入 `devServer` 中。。

Jarvis、webpack-dashboard、webpack visualizer等非官方工具也可提供打包信息,帮你分析webpack编译。

4. 使用最新版本

环境:开发,生产

目标: 构建速度,增量构建速度,打包体积,加载体积

工欲善其事,必先利其器。使用最新的版本,保证最快的速度。请检查node、 webpack、插件、 loader等的版本号,使用最新版本。推荐一款工具 npm-check, 只要在项目根目录运行:

npm-check -u

即查看哪些包有更新:

update.png

5. 删除不必要的插件、loader等工具

环境:开发,生产

目标:构建速度,增量构建速度

任何插件和loader的启用都占用时间,使用最少的插件和loader可以减少webpack的打包时间。

  • 一些工具,只对生产阶段有效,比如压缩、CSS分离、hash,请在开发环境删除。
  • 一些工具,比如 progress-bar-webpack-plugin进度提示条,请衡量对你是否真的有用,每增加一个插件都会让打包更慢。
  • 对于speed-measure-webpack-pluginwebpack-bundle-analyzer 这类量化插件,只在调试webpack时使用,在其他时候请删除。
  • 在webpack打包时,会在terminal中输出moudle、chunk等打包信息,生成这些信息会消耗一定的时间。建议设置为 stats: 'errors-only' 或者 stats: 'minimal'

6. 设置正确的模式mode

环境:开发,生产

目标: 构建速度,增量构建速度,构建体积,加载体积

版本变动:webpack3

webpack提供开箱即用的配置环境,通过 mode 属性告知webpack你的使用环境,webpack将自动开启一系列的优化。

  • 生产环境,请设置 mode:'production'
  • 开发环境,请设置 mode: 'development'
module.exports = {
  //...
  mode: 'production' // 生产环境,也是默认值
  mode: 'development' // 开发环境
}

设置 mode 后,webpack自动将 process.env.NODE_ENV 设为相应的值(生产为 production , 开发为 develpment ),react等包根据此值进行优化,所以不要将process.env.NODE_ENV 改为其他值。

webpack3没有 mode 选项,需要用 DefinePlugin手动设置process.env.NODE_ENV,明确生产或者开发环境 。

webpack自动启用的优化,如无需配置,本文不再介绍。

7. 启用多进程,并行编译

环境:开发,生产

目标: 构建速度增量构建速度

webpack默认是单进程编译,不利于发挥多核CPU的威力。可以采取些措施,使用多进程,这将大幅提高打包速度:

  • 对于 TerserWebpackPlugin 等插件,本身支持多进程,默认开启,也可通过 parallel 选项进行个性化配置。

  • 对于其他loader,可以使用 happyPack 或者 thread-loader 进行并行编译。这两个功能类似,对打包速度的提升也类似。对于babel-loader 尤其需要并行编译,提高打包速度。

    happyPack 使用的人较多,使用限制较少, 但其配置较复杂,例如:

    // npm i -D happypack
    
    const HappyPack = require('happypack')
    
    module.exports = {
        // ...
        module: {
            loaders: [{
                test: /\\.js$/,
                loader: 'babel-loader',
                happy: { id: 'js' }
            }]
        },
        plugins: [
            new HappyPack({ id: 'js' })
        ]
    }
    
    

    thread-loader 有些使用限制,但配置非常简单:

    // npm i -D threa-loader
    
    module.exports = {
        // ...
      module: {
        rules: [
          {
              test: /\\.jsx?$/,
              use: ['thread-loader', 'babel-loader']
          }
        ]
      }
    }
    
    

    thread-loader 将其后的loader 放在单独的池中运行,要求loader不能产生新的文件、不能使用插件、无法获取webpack选项。

开启多进程会消耗大量时间,各进程之间的通信也非常耗时。请仅对长耗时的loader启用。进程数过多会导致打包变慢,进程的默认数通常是CPU数-1,即 os.cpus().length - 1

8. 使用持久缓存

环境:开发,生产

目标:构建速度,增量构建速度

版本变动:webpack5

webpack构建一次耗费很长时间,如果将构建的结果进行缓存,第一次构建的时间可能略有增加,但之后的构建时间将大幅度缩短,这种技术叫持久缓存。

  • 建议使用 hardSourcePlugin 进行全局缓存,开箱即用,无需配置,使用效果非常好,能减少50%以上的构建时间。hardSourcePlugin 在使用中可能存在一些问题,可能导致CSS分离失效,如有问题请查阅官方文档。使用hardSourcePlugin 后,不需再使用cache-loader 或者 DLL plugin 等缓存工具。hardSourcePlugin 配置示例:

    // npm i -D hard-source-webpack-plugin
    const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
    
    module.exports = {
        // ...
        plugins: [
            new HardSourceWebpackPlugin ()
        ]
    }
    
    
  • 对于 TerserWebpackPlugin ,请确保缓存开启,即 cache 不为 falsehardSourcePlugin对TaresePlugin似乎没有效果,需单独处理。

  • 对于webpack5,请直接配置 cache: { type: "filesystem” } ,不再需要 hardSourcePlugin 等缓存工具,TarserPlugin 自带的缓存特性也会失效,无需开启。

另外一种缓存技术叫做”长期缓存" (long-term caching), 是指浏览器缓存一些资源,可以更快的打开页面,需与“持久缓存”相区别,后文有更多介绍。

9. 选择合适的devTool

环境:开发,生产

目标:构建速度、增量构建速度

webpack打包后的代码是经过压缩处理,缺少可读性。devTool可以控制代码的输出品质,添加 source-map ,增加代码可读性,方便错误定位。注意代码输出品质的提高,会使打包速度变慢,需均衡选择。

module.exports = {
  //...
  devTool: 'source-map' // 产生源码品质的source-map,有暴露源码风险,不建议使用
}

生产环境常用的值:

  • none :不使用devtool,速度最快,无法定位错误,是生成环境默认值
  • hidden-source-map :产生 source-map , 需要手动加载到浏览器devtool,或使用其他工具加载,才可查看源码和追踪错误,推荐使用
  • nosources-source-map :无法看到源码,但能在浏览器devtoo看到错误栈,错误发生的准确位置、行号等信息。这是线上环境最简单的方案,但暴露源码结构。

开发环境常用的值:

  • eval: 源码被放入 eval 函数中,缺少可读性,只知道错误的大概位置,没有错误行号、列号提示,构建速度最快,这是开发环境默认值
  • eval-cheap-source-map : 看到近似源码的代码,有错误行号提示,没列号提示。较快的构建速度和增量构建速度。
  • eval-cheap-module-source-map : 能看到源码,有行号错误提示,没列号提示。较慢的构建速度和较快的增量构建速度,推荐此选项
  • eval-source-map : 能看到源码,并有行号和列号的错误提示。很慢的构建速度和一般的增量构建速度。

devTool 还有其他配置值,需均衡考虑构建速度和错误定位,并防止源码在生产环境中暴露。你可以用 SourceMapDevToolPlugin 代替 devTool 进行个性化配置。

如未能生成 source-map,检查一些插件或者loader是否配置 sourceMap: true ,这包括postcss-loader、resolve-url-loader、TeserWebpackPlugin、OptimizeCSSAssetsPlugin 等。

10. 合理配置loader的解析范围

环境:开发,生产

目标:构建速度,增量构建速度

一些loader,比如 babel-loader 转换JS的过程是非常耗时的,如果能减少loader处理的文件数量,会大大减少编译时间。

可以通过 test 正则匹配文件, include 指定要包含的文件, exclude 指定要排除的文件。优先级为: exclude > include > test ,例如:

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\\.styl$/,
        include: [
          path.resolve(__dirname, 'app/styles'),
          path.resolve(__dirname, 'vendor/styles')
        ],
                // 在app/styles 中排除掉 app/styles/common
        exclude: [path.resolve(__dirname, 'app/styles/commen')],
                use: ['style-loader', 'css-loader', 'stylus-loader']
      }
    ]
  }
}

顺便提下loader的执行顺序,列表中较后的选项先执行,比如在上例中,先执行 stylus-loader

解析.style,再执行 css-loader解析CSS,最后执行 style-loader 将CSS内联处理。

11. 忽略jquery等模块的解析

环境:开发,生产

目标:构建速度、增量构建速度

jquery, lodash等是独立的、不需要外部依赖即可运行的模块。可以使用 module.noParse 避免对这些库进行解析,以提高构建性能。配置方法如下:

module.exports = {
  //...
  module: {
    noParse: /jquery|lodash/,
  }
};

module.noParse 配置后,将忽略相关模块中 importreuqiredefine 的调用,请确保配置的模块没有其他依赖。

module.noParse 只是不对jquery等模块进行解析(如通过babel-loader转换js文件),打包后的文件中仍包含jquery的代码。 module.noParse 的优先级高于上文中对loader的includetest配置。

12. 使用 externals 引入公共CDN

环境:开发,生产

目标:构建速度、增量构建速度

可以利用公共CDN服务加载包,而不用webpack打包。如下配置,将 jquery 变量挂载在 windows 变量上:

module.exports = {
  //...
  externals: {
    'jquery': 'jQuery'
  }
};

当然,要在HTML代码中引入jquery:

<script src="<http://libs.baidu.com/jquery/2.0.0/jquery.min.js>"></script>

webpack打包后的文件中将不包含jquery的代码。

如果你的多个应用使用相同的依赖,可以将公共依赖分离,再用此种方式单独部署使用。

13. 合理配置 reslove 字段,缩小webpack寻找模块的范围

环境: 开发、生产

目标:构建速度、增量构建速度

resolve 配置webpack如何查找模块,例如 import 'vue'resolve 告诉webpack在哪里找到vue。更精准的配置将减小webpack搜索模块的时间。

module.exports = {
  //...
  resolve: {
    modules: ['node_modules'],   // 仅在node_modules目录寻找模块
    extensions: ['.js', '.json'],   // import 'utils' 类似于 import 'utils.js' 
    mainFields: ['loader', 'main'], // 通过package.json 的loader、main 选项找到入口文件名
        mainFiles: ['index'], // 设index为入口文件名
        symlinks: false,      // 忽略npm link
    alias: { 
            // import 'Pages' 将等同于 import 'src/pages'
            Pages: 'src/pages',  
            // webpack直接确定react模块位置,不需要层层查找 
      react: patch.resolve(__dirname, './node_modules/react/dist/react.min.js') 

        }
    }
};

  • resolve.modules :告诉webpack搜索模块的范围。
  • resolve.mainFieldsresolve.mainFiles :告诉webpack模块的入口文件。
  • resolve.extensions :如果引入文件时不指定文件类型后缀,将通过此字段查找相应的文件类型。
  • resolve.symlinks :如果不使用 NPM link ,可将此设为 false
  • resolve.alias :设置别名,这将提高代码的可读性,并加快模块的寻找速度。

如果选项是列表的,请尽量减少列表项,并将高频值写在前面, 如 extensions: ['.js', '.json']js 在前。

未掌握 reslove 各选项含义前,请使用默认值, reslove 对webpack编译的提速不明显,但错误的配置可能导致一些bug,比如明明存在某个模块但webpack无法找到该模块。

14. 使用ES module,保证tree-shaking删除无用代码

环境:开发,生产

目标:构建体积

版本变动:webpack3

webpack4支持tree-shaking,可以删除未使用的代码,请使用ES module的导入方式,确保tree-shaking的有效,例如:

// common.js
const run = () => {console.log('run')}
const fly = () => {console.log('fly')}

export {run, fly}

// index.js
import {run, fly} from './common'
run()

如此,fly将会从代码中删除。按下述导入方式,tree-shaking不会生效:

// common.js 同上

// index.js
import common from './common'
common.run()

// common 依赖将被整个打包,无法实现tree-shaking

由于JS动态编译的特性,tree-shaking的效果有限。对于很多依赖,webpack无法很好的执行tree-shaking。另外webpack3不支持tree-shaking。

在开发其他项目的依赖包时,可考虑在 package.js 中添加 sideEffects:false,其他项目在导入你的包时,会尝试进行tree-shaking。

15. 使用TerserWebpackPlugin 最小化JS

环境:生产

目标:构建速度,打包体积

TerserWebpackPlugin 可以最小化JS,即压缩js,并且支持最小化ES6,webpack4生产模式下自动启用,你也可以进行一些配置:

  • 请确保 parallel 多进程开启,提高打包速度。
  • 像loader一样,通过 testincludeexclude 缩小压缩文件范围。也可以用 chunkFilter 排除一些chunk。
  • 非webpack5版本,请确保 cache 缓存开启,webpack5中此选项失效。
  • 如果需要source-map,请配置 sourceMaptrue ,这将生成source-map。注意cheap-source-map 等包含cheap 的source-map 不会产生source-map。请使用hidden-source-map 或者nosources-source-map

配置示例如下:

const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
        minimize: true,
    minimizer: [
      new TerserPlugin({
        exclude: /\\/excludes/,
                chunkFilter: (chunk) => {
                    return chunk.name !== 'vendor' // 排除名为 `vendor` 的chunk
        },
                parallel: true, // 多进程,默认开启
                cache: true, // 持久缓存,默认开启
                sourceMap: true, // 支持产生source-map,默认不开启
                terserOptions: {
                    //...  设置terser的压缩选项,比如是否清除注释,详见官方文档
                }
      }),
    ],
  },
};

不要使用 UglifyjsWebpackPlugin,此插件不支持ES6,并已经停止维护。

16. 使用 optimize-css-assets-webpack-plugin 最小化CSS

环境:生产

目标:打包体积

使用 optimize-css-assets-webpack-plugin 最小化CSS,减少CSS代码的体积。webpack4 中不包含此插件,需先安装。

// npm i -D optimize-css-assets-webpack-plugin

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({})
    ],
  },
};

17. 优化图片

环境:生产

目标: 打包体积

图片是网站重要的一部分,不仅体积大,而且数量多,占用大量带宽,下面是优化图片的一些方法:

  • 使用 url-loader 将小尺寸图片进行内联。可以配置 limit 选项,将小于此值的图片转换成base64 格式并存在JS代码中,以节少请求数量。 file-loader 有和 url-loader 类似的功能,可以解析静态文件,但不能设置 limit 属性进行内联。
  • 使用 svg-loader 将svg图片进行内联。此插件和 url-loader 类似,但SVG是文本,压缩体积上更有效率。
  • 雪碧图可将多张小图片组装合成一张大图片,减少请求数量。由于雪碧图配置较复杂,不推荐使用。可以利用 webpack-spritesmith 插件实现雪碧图。
  • 使用 image-webpack-loader 压缩图片。它支持JPG,PNG,GIF和SVG等几乎所有格式。 它不会将图片内联,需与url-loader等配合使用,即先压缩图片,再进行内联。可以通过 enforce: 'pre' 确保在其他loader前执行。

配置示例如下:

// npm i -D image-webpack-loader
// npm i -D url-loader
// npm i -D svg-url-loader

module.exports = {
  module: {
    rules: [
            // 压缩图片
      {
                test: /\\.(jpe?g|png|gif|svg)$/,
                loader: 'image-webpack-loader',
                enforce: 'pre', // 在其他loader调用前,先行调用,避免重复代码
            },
            // 内联图片
      {
                test: /\\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
                options: {
          limit: 10 * 1024, // 小于10kb的会被内联
        },
      }
            // 内联svg
            {
        test: /\\.svg$/,
        loader: 'svg-url-loader',
        options: {
          limit: 10 * 1024, // 小于10kb的会被内联
          noquotes: true,  // 删除引号
                    iesafe: true, // 支持IE,但会增加体积
        },
      },
    ],
  },
};

18. 优化第三方依赖包

环境: 开发,生产

目标: 构建速度、增量构建速度、打包体积

几乎一半以上的JS体积来源于第三方依赖包,如果能优化依赖包体积,会大幅减少构建体积。我们通常只用到包的几个方法,但却引入包的全部内容。比如,我们只在中文环境使用Moment.js,却引入了Moment.js的各国语言包,使得项目臃肿。

Googel 在Github repo上收集了一些优化建议,其中包括babel-loard、loadsh、react、bootstrap等的优化建议。请在github上搜索“webpack-libs-optimizations”查看。

19. 对html-webpack-plugin的优化

环境:生产

目标:打包体积

hteml-webpack-plguin 可以很方便的创建入口html文件,但它对自定义模板默认不开启压缩的,请配置 minify 选项,进行最小化代码

// npm i -D html-webpack-plugin
// npm i -D uglifyjs-webpack-plugin  压缩js

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
        // 使用'ejs'后缀防止模板文件被html—loader 解析
        template: path.join(__dirname, 'index.ejs'),
        filename: 'index.html',
        minify: { // 开启最小化操作
            collapseWhitespace: true,
            removeComments: true,
            removeRedundantAttributes: true,
            removeScriptTypeAttributes: true,
            removeStyleLinkTypeAttributes: true,
            useShortDoctype: true,
            minifyCSS: true,
            minifyJS: true, // 使用uglifyjs进行压缩,所以需要安装相关依赖
        }
    })
  ]
}

20. 优化Node核心库的polyfill

环境:生产

目标:打包体积,加载体积

版本变动:webpack5

webpack打包时会自动加入Node环境的polyfill,所以在浏览器中也可使用Node核心库,比如 process.env.node_env ,这会略微增加打包的体积。如果不需要浏览器使用Node的核心库,请配置:

module.exports = {
  // ...
    node: false
}

你也可以对每一个核心库进行精确配置,以下是webpack4的默认配置值(webpack5有差别):

module.exports = {
  // ...
    node: {
      console: false,
      global: true,
      process: true,
      __filename: "mock",
      __dirname: "mock",
      Buffer: true,
      setImmediate: true
    }
}

其中的 consoleglobalprocess 等为node核心库中的相应功能,更多选项需查找 node 核心库。配置的值的含义如下:

  • true :提供polyfill。
  • mock :提供 mock 实现预期接口,但功能很少或没有。
  • empty :提供一个空对象。
  • false :不提供polyfill,如果使用相应的node库,会触发错误。

21. 分离代码

环境:生产

目标:打包体积,加载体积

版本变动:webpack3,webpack5

webpack将各种资源打包成一个JS文件,合并请求,优化用户体验。现在的web应用体积越来越大,单一的js文件体积过大,需拆分成多个js文件,减少加载体积。这也是懒加载、长久缓存等技术使用的基础。分离出的每一个js文件,称之为chunk。

webpack3使用 CommonsCunksPlugn 配置复杂,webpack4提供开箱即用的 optimization.splitChunks 。生产环境自动启用 optimization.splitChunks ,默认配置足以应对大多数情况,不需要专门配置。默认配置遵照的主要规则:

  • 主入口会打包成单独的chunk。
  • 动态导入的每一个模块,会打包成一个chunk。例如按路由分离3个页面,将会打包出3个chunk。
  • 每个chunk中用到的node_modules依赖,会被打单独打包成以 vendors~ 开头的chunk
  • 如果两个chunk中包含相同的内容,相同的内容会被单独打包成一个共享chunk。

比如有个项目:

  • index 入口页,用到react,index组件
  • about 介绍页,用到react,about组件,common组件
  • detail 详情页,用到angular,detail组件,common组件

将打包成:

  • index.js: react, index组件
  • about~detail.js: common组件
  • about.js: about组件
  • detail.js: detail组件
  • vendors~detail.js: angular

如修改默认规则,请详细阅读官方文档,并注意评估打包结果,防止负优化,例如:

module.exports = {
  //...
  optimization: {
    splitChunks: {
            //... 一些配置
        }
  }
}

webpack5对optimization.splitChunks 进行一些优化,配置略有不同。

22. 分离CSS

环境:生产

目标:打包体积,加载体积

版本变动:webpack3

在分离JS的基础上,有时需分离CSS,以实现CSS的按需加载。在webpack3 时使用 ExtractTextWebpackPlugin , 在webpack4 时请使用 mini-css-extract-plugin :

// npm i -D mini-css-extract-plugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [
        new MiniCssExtractPlugin({
            filename: '[hash].css',
            chunkFilename: '[contenthash].css' // 注意使用contenthash
        })
    ],
  module: {
    rules: [
      {
        test: /\\.css$/i,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              esModule: true,
                            hmr: process.env.NODE_ENV === 'development' // 热响应
            },
          },
          'css-loader',
        ],
      },
    ],
  },
};

CC将分离成一个个chunk。在开发阶段可以考虑不分离CSS,以提高打包速度。

23. 使用按需加载,懒加载,预加载

环境:生产

目标:加载体积

版本变动:webpack5

现在网页应用功能越来越多,尺寸越来越大,我们可以只加载用户用到的部分,而不加载其他部分以提高性能。比如只加载首屏需要的chunk,而其他部分的chunk暂时不加载。

我们可以使用上文所说的 split-chunk 分离代码,并通过动态导入实现按需加载/懒加载。Vue,React等都提供动态导入组件的方案,也可以用 function import() 实现懒加载,比如:

import(/* webpackChunkName: "lodash" */ 'lodash')

webpack5使用更加智能的chunk命名方式,不需要诸如 /* webpackChunkName: "lodash" */ 的代码。

对于分离的代码,我们也可在空闲时间,将其预先加载,以提高用户体验,比如:

import(/* webpackPrefetch: true */ 'lodash')

24. 使用长久缓存

环境:生产

目标:加载体积

版本变动:webpack3,webpack5

浏览器打开应用时会下载资源,这会占用大量的时间。如果将一部分资源在浏览器中缓存,下次打开页面时不需要重下载,可以降低首屏渲染时间。

我们在修改项目时,通常只修改一部份,让浏览器仅更新修改部分即可,其余资源使用缓存。webpack打包后,会给每一个静态资源添加hash后缀,如果修改该资源,hash会发生改变,浏览器会重新加载该资源。如果该资源没被修改,hash会保持不变,浏览器会使用缓存,这种技术叫做“长久缓存”(long cache)。需与前文提到的"持续缓存”相区别,持续缓存是用于提高打包速度的。

webpack5对长久缓存进行大量优化,配置更为方便。下面的步骤限于webpack4版本,webpack3也需要类似复杂的配置,但略有不同。

长久缓存依靠 splitChunk 代码分离,请参考上文。

  • 第一步,告知浏览器缓存文件

    需要在服务器上设置缓存文件时长,比如通过Cache-Control

    // Server header
    Cache-Control: max-age=31536000      // 设置缓存一年时间
    
    

    有多种设置缓存的方法,请查看相关文章

  • 第二步,使用 recordsPath 配置项,方便调试

    在设置长久缓存时,我们需要知道webpack打包后的文件是如何划分的,模块id是否改变,recordsPath 配置项可以产生一份json文件,记录每次打包后的结果。

    const path = require('path')
    
    module.exports = {
      // ...
        recordsPath: path.join(
            process.cwd(),  // npm run build 在terminal中执行时的目录路径
            'records.json'  // 输出的文件名
        )
    }
    
    
  • 第三步,为文件添加hash

    需要为打包后的文件设置hash后缀,相当于给该文件添加一个版本号,告知浏览器该文件是否更新,比如设置 [name].[hash].js。hash值有三种:

    • [hash]: webpack每次编译都会产生一个唯一的hash值,每次编译hash都会改变
    • [chunkhash]: webpack每次编译,对每一个chunk(每一个js文件)生成不同的hash值,如果修改chunk,会重新计算chunkhash。如果没修改chunk,chunkhash不会变。对JS资源我们通常添加[chunkhash] 后缀。
    • [contenthash]:webpack根据输出的文件内容,计算并生成contenthash,每次编译后都会根据内容生成contenthash,如果改文件内容不变,生成的contenthash是相同的。通常对图片、字体、样式等资源添加[contenthash] 后缀。
    module.exports = {
        // ...
        output: {
            chunkFilename: '[chunkhash].js' // 为chunk添加chunkhash
        },
        module: {
            rules: [
                {
                    test: /\\.ttf/,
                    loader: 'file-loaser',
                    options: { name: '[contenthash].ext'} //为字体添加contenthash 
                }
    
            ]
        }
    }
    
    

    由于不同的系统、计算机使用的hash计算方式不同,在不同的计算机对相同的项目打包,产生的hash值可能不同,导致长久缓存失效。

  • 第四步,分离runtime

    runtime记录所有的资源文件名和路径,以帮助浏览器找到最新版本的chunk,比如:

    {
        "0":"js/0.840dc3db.js",
        "common":"js/common.50055e90.js",
        "detail":"js/detail.dd333b62.js"
        //...
    }
    
    

    通常每个chunk中都包含runtime信息,只要有一个chunk发生变化,runtime就会变化,进而使所有chunk发生变化。可以通过 optimization.runtimeChunk 将runtime分离为一个单独的chunk,我们将将这个chunk称为 runtime.js,有时也称为manifest.js (比如vue-cli中)。

    module.exports = {
      // ...
        optimization: {
            runtimeChunk: {
            name: 'runtime'   // 分离runtime chunk,并命名为 'runtime'
        },
        }
    }
    
    
  • 第五步,内联runtime文件

    分离的runtime文件,将作为一个独立的js加载,浏览器的请求顺序变为:

    index.html → runtime.js → 首屏chunk。

    可以看出多了次请求,使用 inline-manifest-webpack-plugin 将runtime内联进html文件,浏览器的请求顺序修改为:

    index.html (包含runtime的代码) → 首屏chunk

    也可以使用webpack-manifest-plugin 实现同样功能, inline-manifest-webpack-plugin 的配置示例:

    // npm i -D inline-manifest-webpack-plugin
    
    const InlineManifestWebpackPlugin 
        = require('inline-manifest-webpack-plugin')
    
    module.exports = {
      // ...
      plugins: [
        new InlineManifestWebpackPlugin(),
      ],
    };
    
    
  • 第六步,稳定moudle id

    webpack会给每一个moudle分配一个id,id是按照顺序计算的,如 0,1,2。如果你删除了某个moudle,或者新增某个moudle,可能导致module id的顺序发生变化,比如:

    moudleId   变动情况
    0          原来的0
    1          新增的module
    2          原来的1, id变化
    3          原来的2,id变化
    
    

    moudle id的变化会导致chunk文件发生变化,我们可以用 hashed-module-ids-plugin 或者用 optimization.moduleIds 来稳定moudle id:

    const webpack = require('webpack')
    
    module.exports = {
      // ...
      plugins: [
        new webpack.HashedModuleIdsPlugin(),
      ]
    }
    
    

经过复杂的配置,终于可以实现长久缓存。

25. 使用性能预算,控制体积

环境:生产

目标:打包体积

项目初期的体积较小,但随项目发展,可能没注意到,安装的某个依赖,使项目打包体积急剧增加。使用性能预算,及时发现打包体积的增加。

配置 performance 即可启用性能预算,生成环境自动开启,默认配置如下:

module.exports = {
    // ...
    performance: {
          hints: 'warning', // 尺寸过大后警告, 
                            // 设为'errors'时体积过大将报错
                            // 设为false时关闭性能预算
          maxEntrypointSize: 250000, // 最大入口体积
          maxAssetSize:250000 // 单个资源最大体积
    }
}

打包后,如果有体积过大,将显示:

budgest.png

使用 bundlesize 工具,可以进行更灵活的配置,比如单独设置某个文件的体积预算,同时支持自动化CI。

26. 其他优化建议

  • 并行运行多个webpack实例 。打包多个版本,比如打包国内版本应用、国际版本应用,可以使用 parallel-webpack 并行运行多个webpack实例。
  • 指定browserslist。指定合适的browserslist,babel将据此选择核合适的降级工具,有助于代码体积的减少和打包速度的提升。比如开发环境需要更快的打包速度,可以设为 'last 2 Chrome versions';生产环境需考虑兼容性,可设为 '> 1%, ie >= 11, not dead'
  • 考虑为不同的浏览器打包不同版本,可以为IE、现代浏览器分别打包,再根据浏览器的标识加载不同的版本。也可以根据浏览器的尺寸将图片打包成不同的尺寸,对PC端浏览器返回大图,而对移动端浏览器返回较小尺寸的图片。
  • 如果担心打包后的文件包含多个相同模块,可以用 duplicate-package-checker-webpack-plugin 或者 bundle-duplicates-plugin 进行检查。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345