webpack5 性能优化

131 阅读4分钟

webpack5高级优化

提升开发体验

Source-Map

是一个用来生成源代码和构建后代码一一映射的文件的方案。

// 开发模式 cheap-module-source-map 打包编译速度快 只有行映射 没有列映射
// 生产模式 source-map 包含行/列映射 打包编译速度更慢
module.exports = {
    devtool: 'cheap-module-source-map'
}

提升打包构建速度

HotModuleReplacement

开发时我们修改了其中一个模块代码,webpack默认会将所有模块全部重新打包编译,速度很慢。
所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。
HotModuleReplacement(HMR) 在程序运行时,替换、添加或删除模块,而无需重新加载整个页面。

module.exports = {
    devServer: {
        host: 'localhost', //启动服务器的域名
        port: 3000,
        open: true, // 是否自动打开浏览器,
        hot: true
    }
}
// js文件要单独处理 实际开发中可使用其他loader 比如 react-hot-loader
if (module.hot) {
    module.hot.accept('./src/index.js')
}

OneOf

打包时每个文件都会经过所有loader处理,虽然因为test正则原因实际没有处理上,但是都要过一遍,比较慢。
oneOf是只能匹配上一个loader,剩下的就不匹配了。

module.exports = {
    module: {
        rules: [
            {
                oneOf: [

                ]
            }
        ]
    }
}

Include/Exclude

include/exclude只能选一个配置

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                // include: path.resolve(__dirname, 'src'), 只处理src
                use: {
                    loader: 'babel-loader',
                    options: { // 可以写在babel.config.js
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    }
}

Cache

每次打包时js文件都要经过Eslint和Babel编译,速度比较慢。
我们可以缓存之前的Esline和Babel编译结果,这样第二次打包时速度就会更快了。

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                // include: path.resolve(__dirname, 'src'), 只处理src
                use: {
                    loader: 'babel-loader',
                    options: { // 可以写在babel.config.js
                        presets: ['@babel/preset-env'],
                        cacheDirectory: true, // 开启babel缓存
                        cacheCompression: false, // 关闭缓存压缩 节省压缩时间
                    }
                }
            }
        ]
    },
    plugins: [
        new EsLintPlugin({
            context: path.resolve(__dirname, 'src'),
            exclude: 'node_modules',
            cache: true,
            cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslintcache')
        })
    ]
}

多进程打包

js文件处理主要就是eslint、babel、Terser三个工具,所以我们要提升他们的运行速度。
我们可以开启多进程同时处理js文件。
多进程打包:开启电脑的多个进程同时做一件事,速度更快。请在特别耗时的操作中使用,因为每个进程启动就有大约600ms左右开销。 我们启动进程的数量就是我们cpu的核数

  1. 获取cpu核数,每个电脑都不一样
const os = require('os');
const threads = os.cpus().length;
  1. 下载包 npm i thread-loader -D
  2. 配置
module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                // include: path.resolve(__dirname, 'src'), 只处理src
                use: [
                    {
                        loader: 'thread-loader',
                        works: threads,
                    },
                    {
                        loader: 'babel-loader',
                        options: { // 可以写在babel.config.js
                            presets: ['@babel/preset-env'],
                            cacheDirectory: true, // 开启babel缓存
                            cacheCompression: false, // 关闭缓存压缩 节省压缩时间
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new ESLintPlugin({
            // 检测哪些文件
            context: path.resolve(__dirname, 'src'),
            exclude: 'node_modules', // 默认值
            cache: true,
            cacheLocation: path.resolve(__dirname, 'node_modules/.cache/eslintcache'),
            threads,
        }),
        // new CssMinimizerWebpackPlugin(),
        // new TerserWebpackPlugin({
        //     parallel: threads
        // })
    ],
    optimization: {
        minimizer: [
            new CssMinimizerWebpackPlugin(),
            new TerserWebpackPlugin({
                parallel: threads
            })
        ]
    },
}

减少代码体积

Tree Shaking

开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。
如果没有特殊处理的话我们打包时会引入整个库,但是实际上我们可能只用上极小部分的功能,这样将整个库都打包进来,体积就太大了。
tree shaking 是一个术语,通常用于描述移除javascript中没有使用上的代码。webpack默认开启此功能。

减少Babel生成文件的体积

Babel为编译的每个文件都插入了辅助代码,使代码体积过大。
Babel为一些公共方法使用了非常小的辅助代码,比如 _extend,默认情况下会被添加到每一个需要它的文件。可以将这些代码作为独立模块,避免重复引入。
@babel/plugin-transform-runtime: 禁用了babel自动对每个文件的runtime注入,而是引入@babel/plugin-transform-runtime,并且使所有辅助代码从这里引用。

{
    loader: 'babel-loader',
    options: { // 可以写在babel.config.js
        presets: ['@babel/preset-env'],
        cacheDirectory: true, // 开启babel缓存
        cacheCompression: false, // 关闭缓存压缩 节省压缩时间
        plugins: ["@balel/plugin-transform-runtime"],
    }
 }

Image Minimizer

image-minimizer-webpack-plugin: 用来压缩图片的插件

优化代码运行性能

Code Split

打包代码时会将所有js打包到一个文件中,体积太大了。如果我们只要渲染首页,就应该只加载首页的js文件,其他文件不应该加载。
所以我们需要将打包生成的文件进行代码分割,生成多个js文件,渲染哪个页面就只加载某个js文件,这样加载的资源就少,速度就更快。
代码分割主要做了2件事:

  1. 分割文件:将打包生成的文件进行分割,生成多个js文件。
  2. 按需加载,需要哪个文件就加载哪个文件。
module.exports = {
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'static/js/main.js',
        chunkFilename: 'static/js/[name].chunk.js',
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'static/css/[name].[contenthash:10].css',
            chunkFilename: 'static/css/[name].chunk.[contenthash:10].css',
        })
    ], 
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: { // 单入口文件不需要配置
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true
                }
            }
            // 一个文件改变,contenthash更新,所有依赖他的文件都会跟着变化,缓存失效
            // 把hash值保存在一个runtime文件
            runtimeChunk: (entrypoint) => `runtime~${entrypoint.name}.js`
        }
    }
}
// count.js 会被单独打包成一个文件
import(/* webpackChunkNmae: "count" */'./count.js').then((res => res.default()))
// eslintrc.js 文件中增加 plugins: ['import']

Preload/Profetch

我们前面已经做了代码分割,同时会使用import动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。
但是加载速度还不够好,比如:是用户点击按钮才加载这个资源的,如果资源体积很大,那么用户会感觉到明显卡顿效果。
我们想在浏览器空闲时间,加载后续要使用的资源,我们就需要用上Preload, Prefetch技术。

  • Preload: 告诉浏览器立即加载资源
  • Prefetch: 告诉浏览器空闲时间加载资源
    • 共同点:都只会加载资源,并不执行。都有缓存
    • 区别:
      1. Preload加载优先级更高
      2. Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。
    • 总结:
      1. 当前页面优先级高的资源用Preload加载
      2. 下一个页面需要使用的资源用Prefetch加载
      3. 兼容性较差
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin')
module.exports = {
    plugins: [
        new PreloadWebpackPlugin({
            // rel: 'preload',
            // as: 'script',
            rel: 'prefetch',
        })
    ]
}

core-js

解决js兼容性问题

import "core-js/es/promise"
// barbel.config.js 实现按需加载
module.exports = {
    preset: [
        [
            '@babel/preset-env', 
            {
                useBuiltIns: 'usage',
                corejs: 3,
            }
        ]
    ]
}

PWA

开发webapp项目,项目一旦处于网络离线情况,就没法访问了。
我们希望给项目提供离线体验。
渐进式网络应用程序(progressive web application): 是一种可以提供类似于native app体验的webapp技术,其中最重要的是在离线时应用程序能够继续运行功能。
内部通过 Service Workers 技术实现的。

// npm i workbox-webpack-plugin
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
    plugins: [
        new WorkboxPlugin.GenerateSW({
            clientsClaim: true,
            skipWaiting: true,
        })
    ]
}
// main.js中注册
if ('serviceWorker' in navigator) {
   window.addEventListener('load', () => {
     navigator.serviceWorker.register('/service-worker.js').then(registration => {
       console.log('SW registered: ', registration);
     }).catch(registrationError => {
       console.log('SW registration failed: ', registrationError);
     });
   });
 }