webpack4优化

下面是我对一个庞大的多页面项目优化的总结,有些评论仅代表我在优化过程遇到的。优化方法、用法我都列举了,望君自行斟酌取舍

一、分析工具

  • 1、speed-measure-webpack-plugin
// webpack.dev.conf.js / webpack.prod.conf.js

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

const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap(YourWebpackConfig);
  • speed-measure-webpack-plugin,它能够测量出在你的构建过程中,每一个 LoaderPlugin的执行时长
  • tips:如果你有自定义Plugin,有用到html-webpack-plugin提供的hooks,请先移除,否则会报错
  • 2、cpuprofile-webpack-plugin
// webpack.base.conf.js

const CpuProfilerWebpackPlugin = require('cpuprofile-webpack-plugin');
 
module.exports = {
  plugins: [
    new CpuProfilerWebpackPlugin()
  ]
}
  • 打包后会在你的项目下生成profile文件夹,文件夹里生成的分析的html文件,用浏览器打开就可以了

二、优化途径:缓存、多核、拆分、抽离

打包慢发现主要因为这两个阶段:

  • 1、 babelloaders解析阶段
  • 2、 jscss压缩阶段

(一)缓存

tips:存在更新依赖后依旧命中缓存的bug,开发机上删除node_modules/.cache解决,但是如果集成在自动化CI流程就麻烦点,除非依赖不更新,否则不建议在CI流程使用

  • 1、vue-loader缓存
// webpack.base.conf.js

            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
+                    cacheDirectory: './node_modules/.cache/vue-loader',
+                    cacheIdentifier: 'vue-loader',
                }
            },
  • 2、babel-loader缓存
// webpack.base.conf.js

            {
                test: /\.js$/,
                loader: 'babel-loader',
+                options: {
+                    cacheDirectory: true,
+                },
                exclude: [path.resolve(__dirname, '../node_modules')]
            },
  • 3、uglifyjs-webpack-plugin缓存
// webpack.prod.conf.js

            new UglifyJsPlugin({
                uglifyOptions: {
                    warnings: false,
                    compress: {
                        drop_console: true
                    },
                },
                sourceMap: false,
+                cache: true
            }),
  • 4、通过cache-loader
// webpack.base.conf.js

      {
        test: /\.js$/,
-        loader: 'babel-loader',
+        use: ['cache-loader', 'babel-loader'],
        include: path.resolve('src'),
      },
// webpack.base.conf.js

    {
        test: /\.(less|css)$/,
        use: [
          _mode === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
+          'cache-loader', // 受MiniCssExtractPlugin实现的影响,放在MiniCssExtractPlugin之后才能生效
          {
            loader: 'css-loader',
            options: {
               importLoaders: 1,
               import: true,
             },
          },
          'postcss-loader',
        ],
      },

(二)多核

多核虽好,请勿迷恋,过多反而拉慢速度。

  • 1、uglifyjs-webpack-plugin多核运行
// webpack.prod.conf.js

            new UglifyJsPlugin({
                uglifyOptions: {
                    warnings: false,
                    compress: {
                        drop_console: process.env.WEHOTEL_ENV !== 'test'
                    },
                },
                sourceMap: false,
                extractComments: false,
                cache: true,
+                parallel: true,
            }),
  • 2、通过happypack
// webpack.base.conf.js

const HappyPack = require('happypack');
    ......
            {
                test: /\.js$/,
-                use: ['cache-loader', 'babel-loader'], // 移到下面的loaders
+                loader: 'happypack/loader?id=happy-babel', // 这里的id和下面plugin的id保持一致
                include: [resolve('src'), resolve('test')],
                exclude: [path.resolve(__dirname, '../node_modules')]
            },
    ......
    plugins: [
+        new HappyPack({
+            id: 'happy-babel',  // 这里的id和上面loader的id保持一致
+            loaders: ['cache-loader', 'babel-loader'], // 来自上面rule的use
+            threadPool: HappyPack.ThreadPool({ size: require('os').cpus().length }), // 设置核数量
+            verbose: false, // 是否打印信息
+        }),
    ......
// webpack.base.conf.js
// 测试下来不理想,我本人没有采用,仅供参考

    {
        test: /\.(less|css)$/,
        use: [
          _mode === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
          'cache-loader', // 这里因为MiniCssExtractPlugin的影响,放在MiniCssExtractPlugin之后才能生效
-          {
-              loader: 'css-loader',
-              options: {
-                 importLoaders: 1,
-                 import: true,
-               },
-          },
+          'happypack/loader?id=happy-css',
-          'postcss-loader',   
+          'happypack/loader?id=happy-postcss',
        ],
      },
    ......
    plugins: [
+        new HappyPack({
+            id: 'happy-css',  // 这里的id和上面loader的id保持一致
+            loaders: [
+                  {
+                      loader: 'css-loader',
+                      options: {
+                         importLoaders: 1,
+                         import: true,
+                       },
+                    }
+              ], // 来自上面的loader
+            threadPool: HappyPack.ThreadPool({ size: require('os').cpus().length }), // 设置核数量
+            verbose: false, // 是否打印信息
+        }),
+        new HappyPack({
+            id: 'happy-postcss',  // 这里的id和上面loader的id保持一致
+            loaders: ['postcss-loader'], // 来自上面的loader
+            threadPool: HappyPack.ThreadPool({ size: require('os').cpus().length }), // 设置核数量
+            verbose: false, // 是否打印信息
+        }),
    ......
  • 通过happypack,为loader提供多个进程执行,明显加速,但是注意happypack的数量,过多反而变慢。happypack支持的loader列表
  • 3、通过thread-loader
    官方推荐使用thread-loader,但是测试下来,真的不行,thread-loader自身每个worker都需要花费时间,就算提前开启预热也没用,或者如同官方说的,请仅在耗时的loader上使用
// webpack.base.conf.js
// 测试下来不理想,我本人没有采用,仅供参考

+ const threadLoader = require('thread-loader');

+ const jsWorkerPool = {
+   poolTimeout: 2000
+ };

+ const cssWorkerPool = {
+   workerParallelJobs: 2,
+   poolTimeout: 2000
+ };

+ threadLoader.warmup(jsWorkerPool, ['babel-loader']);
+ threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);

      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
+          'thread-loader',
          'babel-loader'
        ]
      },
      {
        test: /\.s?css$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
+          'thread-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
          'postcss-loader'
        ]
      }

(三)拆分

// webpack.prod.conf.js / webpack.dev.conf.js

+    optimization: {
+        moduleIds: 'hashed', // 有利于缓存
+        chunkIds: 'size', // 有利于缓存
+        mangleWasmImports: true, // 告知 webpack 通过将导入修改为更短的字符串
+        splitChunks: {
+            chunks: 'initial', // 用于命中chunk,function (module, chunk) | RegExp | string
+            cacheGroups: {
+                common: {
+                    chunks: 'initial', // all、async、initial,默认async
+                    minChunks: 2, // 最小共用模块数
+                    name: 'common', // 模块名
+                    priority: 9, // 优先级
+                    enforce: true // 忽略splitChunks设置
+                },
+                vendor: {
+                    test: /node_modules/, // 用于命中chunk,function (module, chunk) | RegExp | string
+                    chunks: 'initial', // all、async、initial,默认async
+                    name: 'vendor', // 模块名
+                    priority: 10, // 优先级
+                    enforce: true // 忽略splitChunks设置
+                }
+            }
+        },
+        runtimeChunk: {
+            name: 'manifest'  // 将入口模块中的runtime部分提取出来
+        }
+    },
    ......
    plugins: [
-      ...... // 这里省略删除CommonsChunkPlugin代码
        new HtmlWebpackPlugin({
            title: 'title',
            filename: 'index.html',
            template: './src/index.html',
            inject: true,
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true
            },
            chunksSortMode: 'dependency',
+            chunks: ['manifest', 'vendor', 'common', name] // 单页面可以不用配置chunks
        })
  • 最难的是找到一个适当的拆分设置,上面的设置仅供参考
  • 适当的拆分,可以优化整个打包文件的大小
  • 适当的拆分,可以优化开发环境热更新的速度
  • webpack4CommonsChunkPlugin废弃,由optimization.splitChunksoptimization.runtimeChunk替代,前者拆分代码,后者提取runtime代码
  • 官方文档 优化(optimization)
  • 官方文档 SplitChunksPlugin

(四)抽离

  • dll抽离不建议使用:
    1、要提前打包,再集成到webpack打包里面,不利于集成到自动化流程
    2、依赖更新又要重新打包,有维护成本,忘记就GG
    3、提前打包的js要插入到html
    4、测试下打包性能提升,效果不明显,在开发环境反而拉慢了速度
  • externals抽离不建议使用:
    1、要考虑各个引用的和项目使用的版本一致
    2、升级依赖包,要及时把引用的版本也更新,有维护成本,忘记就GG
    3、引用包过多,拉慢加载速度,除非有http2的多路复用
    4、引用的文件,如果用第三方会有cdn不稳定,要自己部署cdn
    5、不同的包之间可能有重复引用,增大总体积
    6、就算你把所有的引用打包成一个文件,部署cdn再引用,上面的问题也有的还是存在
  • 1、dll
// ddl.config.js

const webpack = require('webpack');

const vendors = [
 'react',
 'react-dom',
 'react-router',
 // ...其它库
];

module.exports = {
 output: {
  path: 'build',
  filename: '[name].js',
  library: '[name]',
 },
 entry: {
  "lib": vendors,
 },
 plugins: [
  new webpack.DllPlugin({
   path: 'manifest.json', // manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包
   name: '[name]', // dll暴露的对象名,要跟 output.library 保持一致
   context: __dirname, // 解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致
  }),
 ],
};
  • 首先新增配置文件ddl.config.js
// package.json

"scripts": {
+    "build:dll": "webpack --mode production --config ddl.config.js",
    ......
  • 运行npm run build:dll,会输出两个文件:lib.jsmanifest.json
// webpack.prod.conf.js

plugins: [
+  new webpack.DllReferencePlugin({
+   context: __dirname, // 需要跟之前保持一致,这个用来指导 Webpack 匹配 manifest 中库的路径
+   manifest: require('./manifest.json'), // 用来引入刚才输出的 manifest.json 文件
+  }),
  ......
  • 通过webpack.DllReferencePlugin引入dll
// webpack.prod.conf.js

+ var HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin');

plugins: [
  new webpack.DllPlugin({
   path: 'manifest.json', // manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包
   name: '[name]', // dll暴露的对象名,要跟 output.library 保持一致
   context: __dirname, // 解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致
  }),
+  new HtmlWebpackTagsPlugin({ tags: ['lib.js'], append: false})
  • 通过html-webpack-tags-pluginhtml插入dll打包出来的js文件
  • 2、externals
// webpack.base.conf.js

  externals: {
    // key是我们 import 的包名,value 是CDN为我们提供的全局变量名
    // 所以最后 webpack 会把一个静态资源编译成:module.export.react = window.React
    "react": "React",
    "react-dom": "ReactDOM",
    "redux": "Redux",
    "react-router-dom": "ReactRouterDOM"
  }

与此同时,我们需要在html中插入script标签

// index.html

+ <script type="text/javascript" src="https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js"></script>
+ ...... // 其他引用的script

三、其他优化

  • 1、缩小编译范围
    优化效果并不明显
// webpack.base.conf.js

+ const resolve = dir => path.join(__dirname, '..', dir);
// ...
+ resolve: {
+    modules: [ // 指定以下目录寻找第三方模块,避免webpack往父级目录递归搜索
+        resolve('src'),
+        resolve('node_modules'),
+        resolve(config.common.layoutPath)
+    ],
+    mainFields: ['main'], // 只采用main字段作为入口文件描述字段,减少搜索步骤
+    alias: {
+        vue$: "vue/dist/vue.common",
+        "@": resolve("src") // 缓存src目录为@符号,避免重复寻址
+    }
+ },
+ module: {
+    noParse: /jquery|lodash/, // 忽略未采用模块化的文件,因此jquery或lodash将不会被下面的loaders解析
+    // noParse: function(content) {
+    //     return /jquery|lodash/.test(content)
+    // },
+    rules: [
+        {
+            test: /\.js$/,
+            include: [ // 表示只解析以下目录,减少loader处理范围
+                resolve("src"),
+                resolve(config.common.layoutPath)
+            ],
+            exclude: file => /test/.test(file), // 排除test目录文件
+            loader: "happypack/loader?id=happy-babel" // 后面会介绍
+        },
+    ]
+ }
  • 减少不必要的编译,即modulesmainFieldsnoParseincludesexcludealias都用起来
  • 2、tree-shaking
    tree shaking设计的初衷应该是shaking掉第三方引入的样式中无用的代码。业务代码,尤其像.vue这样的组件化开发tree shaking的使用有限。反正我打开后各种问题,就弃坑了
// package.json

    ......
    sideEffects: false
    ......
  • 设置sideEffects: false告诉编译器该项目或模块是pure的(所有文件都没有副作用),可以进行无用模块删除
// package.json

"sideEffects": [
    "*.css*",
    "*.vue"
],
  • .css文件、.vue文件模块有副作用,需要在打包的时候不要错误删除了这些模块的代码

四、总结

打完收工。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352