当下流行的打包工具Webpack从入门到熟练系统学习教程(四)webpack性能优化

上一篇:【文件处理

1. 开发环境优化

1.1 优化打包构建速度

1.1.1 HMR(模块热替换)

HMR 全称 Hot Module
Replacement,中文语境通常翻译为模块热更新,它能够在保持页面状态的情况下动态替换资源模块,提供丝滑顺畅的 Web 页面开发体验。详情看下面解释

HMR未出现之前:某个文件一小部分修改都要重新打包整个文件
HMR出现后:当一个文件其中一个模块发生变化,只会重新打包这一个模块,而不是打包所有模块,会极大地提升构建速度
主要是优化打包构建速度;优化代码调试;从 webpack-dev-server v4.0.0 开始,热模块替换是默认开启的。

CSS文件的使用:
CSS样式文件:可以使用HMR功能;因为style-loader内部实现了,所以不需要操心,只要使用HMR就必须使用style-laoder功能
写法如上:在devServer里面编写属性即可

    devServer: {
    
    
        static: {
    
    
            directory: path.join(__dirname, 'build'),
        },
        compress: true,
        hot: true      //开启HMR   添加这一行在devServer配置项里
    },

js文件:默认不能使用HMR功能 需要修改js代码(但是只能处理非入口文件)添加下面的代码到入口文件

if (module.hot) {
    
    
    // 一旦module.hot为true,说明开启了HMR功能
    module.hot.accept('./dir.js', function() {
    
            //dir.js不是入口文件,自己写的一个测试文件
        // 方法会监听文件的变化,发生过i便其他模块不会被重新打包
        //会执行后面的回调函数
        print();
    })
}

html文件:默认不能使用,将文件放入入口文件数组内可以实现;但是html只有一个文件,所以不需要做HMR文件

entry: ['./src/index.js', './src/index.html'],    // 一个是入口js文件  一个是html文件

1.2 优化代码调试

1.2.1 sourc-map

Source Map 是一个信息文件,里面存储了代码打包转换后的位置信息,实质是一个 json 描述文件,维护了打包前后的代码映射关系
有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码,能够极大的方便后期的调试
使用:

 devtool: 'inline-source-map',     // 添加属性即可

属性解释:

【inline | hidden| eval】【nosources】【cheap-[module]】source-map
【inline | hidden| eval】:【存储位置说明内部还是外部】这三个只能存在一种,三选一
【nosources】:【隐藏代码】要么有,要么没有
【cheap-[module]】:【打包的一些信息,报错信息】要么有一个cheap,要么都有cheap-module
source-map :必须要有

内联和外部区别:

外联是外部生成独立的文件 —— 适合生产模式
内联不生成文件,会和js文件放在一起 —— 内联构建速度更快,适用于开发模式

属性搭配:
inline-source-map:
内联(只生成一个内联source-map,会和js文件放在一起) 错误代码准确信息 和 源代码错误位置
hidden-source-map:
外部(会分割一个单独的文件) 错误代码错误原因 但是没有错误位置提示 不能跟踪源代码错误 只能提示到构建后代码的错误位置
eval-source-map:
内联 (每一个文件都生成对应的source-map,和js文件放在一起) 错误代码准确信息 和 源代码错误位置
nosources-source-map:
外部 错误代码准确信息 和 没有源代码任何信息
cheap-source-map:
外部 错误代码准确信息 和 源代码错误位置 只能精确到行
cheap-module-source-map:
外部 错误代码准确信息 和 源代码错误位置 module会将loader的source map加入

最终方案选择:
开发环境: 速度快 调试友好
速度(eval>inline>cheap)
eval-cheap-source-map / eval-source-map
调试友好
cheap-module-source-map
cheap-source-map

  • 最终方案: eval-source-map / eval-cheap-module-source-map

生产环境: 源代码要不要隐藏 调试要不要更友好 内联会让代码体积变大 所以生产环境不使用内联

  • 最终方案:source-map / cheap-module-source-map

2.生产环境优化(速度+性能)

2.1 优化打包构建速度

2.1.1 Oneof

由于loader的rules数组里配置的规则会在每个文件打包编译的时候都去匹配一次,如果多次匹配会做多次loader处理;也就是说一个文件可能会被多个loader处理 怎么样避免这种问题呢
使用Oneof[]

这是之前的代码

  module: {
    
    
    rules: [
      {
    
    
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader',
        enforce: 'pre',
        options: {
    
    
          fix: true
        }
      },
      {
    
    
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
    
    
          presets: [
            ['@babel/preset-env']
          ]
        }
      },
      {
    
    
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
    
    
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      },
      {
    
    
        test: /\.(png|jpg|jpeg|gif|bmp)$/,
        loader: 'url-loader',
        options: {
    
    
          limit: 8 * 1024,
          name: '[hash:10].[ext]',
          outputPath: 'imgs'
        }
      },
      {
    
    
        test: /\.html$/,
        loader: 'html-loader',
        options: {
    
    
          esModule: false
        }
      },
      {
    
    
        exclude: /\.(css|js|html|json|less|png|jpg|gif)$/,
        loader: 'file-loader',
        options: {
    
    
          name: 'media/[hash:9].[ext]'
        }
      }
 
    ]
},

使用oneof 把oneof放在rules配置项里面


module: {
    
    
    rules: [{
    
    
            oneOf: [{
    
    
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loader: 'eslint-loader',
                    enforce: 'pre',
                    options: {
    
    
                        fix: true
                    }
                },
                {
    
    
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader',
                    options: {
    
    
                        presets: [
                            ['@babel/preset-env']
                        ]
                    }
                },
                {
    
    
                    test: /\.css$/,
                    use: ['style-loader', 'css-loader']
                },
                {
    
    
                    test: /\.less$/,
                    use: ['style-loader', 'css-loader', 'less-loader']
                },
                {
    
    
                    test: /\.(png|jpg|jpeg|gif|bmp)$/,
                    loader: 'url-loader',
                    options: {
    
    
                        limit: 8 * 1024,
                        name: '[hash:10].[ext]',
                        outputPath: 'imgs'
                    }
                },
                {
    
    
                    test: /\.html$/,
                    loader: 'html-loader',
                    options: {
    
    
                        esModule: false
                    }
                },
                {
    
    
                    exclude: /\.(css|js|html|json|less|png|jpg|gif)$/,
                    loader: 'file-loader',
                    options: {
    
    
                        name: 'media/[hash:9].[ext]'
                    }
                }
            ]
        }

    ]
},

因为不能有两个配置处理同一种类型的文件 比如 下面js【eslint-loader 和 babel-loader 】都在处理js 因此需要这样修改(把第一个test提取到oneof外面)


  module: {
    
    
    rules: [
      {
    
    
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader',
        enforce: 'pre',
        options: {
    
    
          fix: true
        }
      },
      {
    
    
        oneOf: [
          {
    
    
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            options: {
    
    
              presets: [
                ['@babel/preset-env']
              ]
            }
          },
          {
    
    
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
          },
          {
    
    
            test: /\.less$/,
            use: ['style-loader', 'css-loader', 'less-loader']
          },
          {
    
    
            test: /\.(png|jpg|jpeg|gif|bmp)$/,
            loader: 'url-loader',
            options: {
    
    
              limit: 8 * 1024,
              name: '[hash:10].[ext]',
              outputPath: 'imgs'
            }
          },
          {
    
    
            test: /\.html$/,
            loader: 'html-loader',
            options: {
    
    
              esModule: false
            }
          },
          {
    
    
            exclude: /\.(css|js|html|json|less|png|jpg|gif)$/,
            loader: 'file-loader',
            options: {
    
    
              name: 'media/[hash:9].[ext]'
            }
          }
        ]
      }
 
    ]


2.1.2 babel缓存

babel缓存开启后会将运行结果保存起来,下一次调用速度就会快很多 就配置一个简单的选项就行

在module下面的rules里配置一个属性开启即可cacheDirectory: true;看下图

   module: {
    
    
        rules: [
            // css文件处理
            {
    
    
                test: /\.css$/,
                use: [...commonCssLoader]
            },
            {
    
    
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
    
    
                    loader: 'babel-loader',
                    options: {
    
    
                        presets: [
                            [
                                '@babel/preset-env',
                                {
    
    
                                    useBuiltIns: 'usage',
                                    corejs: {
    
    
                                        //core-js的版本
                                        version: 3
                                    },
                                    //需要兼容的浏览器
                                    targets: {
    
    
                                        chrome: '60',
                                        firefox: '60',
                                        ie: '9',
                                        safari: '10',
                                        edge: '17'
                                    }
                                }
                            ]
                        ],
                        // babel缓存  第二次构建时会读取之前的缓存
                        cacheDirectory: true
                    }
                }
            },
        ]
    },
cacheDirectory: true

2.1.3 多进程打包

同一时间可以做多件事,针对消耗时间比较长的进行优化效果会明显

在需要多进程打包的配置项里填写属性;一般是js文件需要

                    'thread-loader', {
    
    
                        loader: 'babel-loader',
                        options: {
    
    
                            workers: 2, // 进程为2
                        },

完整代码

    module: {
    
    
        rules: [
            // css文件处理
            {
    
    
                test: /\.css$/,
                use: [...commonCssLoader]
            },
            {
    
    
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    // 开启多进程打包  (工作时间消耗长时才需要多进程打包)
                    'thread-loader', {
    
    
                        loader: 'babel-loader',
                        options: {
    
    
                            workers: 2, // 进程为2
                        },
                        options: {
    
    
                            presets: [
                                [
                                    '@babel/preset-env',
                                    {
    
    
                                        useBuiltIns: 'usage',
                                        corejs: {
    
    
                                            //core-js的版本
                                            version: 3
                                        },
                                        //需要兼容的浏览器
                                        targets: {
    
    
                                            chrome: '60',
                                            firefox: '60',
                                            ie: '9',
                                            safari: '10',
                                            edge: '17'
                                        }
                                    }
                                ]
                            ],
                            // babel缓存  第二次构建时会读取之前的缓存
                            cacheDirectory: true
                        }
                    }
                ]
            },
        ]
    },

2.1.4 externals(彻底不打包)

我们希望某个依赖是通过别的链接引入进来,不会被打包
第一步:添加属性到module.exports

    externals:{
    
    
        //  拒绝jquery这个包被打包
        jquery:'jQuery'
    }

第二步:配置

import $ from 'jquery'
console.log($);

第三步:别的方式引入(html引入cdn链接)

    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.slim.min.js"></script>

2.1.5 DLL(需要打包一次;后期就不用打包了)

第一步:创建一个webpack.dll.js文件
说明要单独打包某些库;会生成两个文件【库文件(jquery.js)+不打包文件(mainfest.json)】

const {
    
     resolve } = require('path')
// 引入插件(在webpack里面;所以不需要下载)
const webpack = require('webpack')
    /*
    对某些库(jquery react vue)进行单独打包
    */
modules.exports = {
    
    
    entry: {
    
    
        // 最终生成的名字name叫jquery
        // ['jquery']:说明要打包的库是jquery
        jquery: ['jquery']
    },
    output: {
    
    
        // 打包库的名称是【name】.js
        filename: "[name].js",
        path: resolve(__dirname, 'dll'),
        library: '[name]_[hash]' //打包的库里面向外暴露出去的叫什么名字  jquery+哈希值
    },
    plugins: [
        // 打包生成一个mainfest.json文件  -- 提供和jquery映射
        new webpack.DllPlugin({
    
    
            name: '[name]_[hash]', // 映射库的暴露内容的名称
            path: resolve(__dirname, 'dll/mainfest.json')
        }),
    ],
    mode: 'production'
}

第二步:在webpack.config.js写入

需要下载插件npm i add-asset-html-webpack-plugin

// 引入插件(会将某个文件输出并在html中自动引入资源)
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
const {
    
     resolve } = require("path");
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    
    
    entry: './src/index.js',
    output: {
    
    
        filename: 'built.js',
        path: resolve(__dirname, 'build')
    },
    module: {
    
    
        rules: []
    },
    plugins: [
        // html-webpack-plugin
        // 默认会创建一个空的html,自动引入打包输出的所有资源(js/css)
        // 需求:需要有结构的html文件
        new HtmlWebpackPlugin({
    
    
            // 复制‘./src/index.html’文件 ,并自动引入打包输出的所有资源(js/css)
            template: './src/index.html'
        }),
        // 告诉webpack哪些库不参与打包;忽略打包jquery
        new webpack.DllReferencePlugin({
    
    
            manifest: resolve(__dirname, 'dll/mainfest.json')
        }),
        // 会将某个文件打包出去,并在html中自动引入资源
        new addAssetHtmlWebpackPlugin({
    
    
            filepath: resolve(__dirname, 'dll/mainfest.json')
        })
    ],
    mode: 'development'
}

2.2 优化代码运行的性能

2.2.1 缓存

使用webpack打包模块后,webpack会生成一个目录用来存放打包好的模块文件,只要该目录里的内容被部署到server上,浏览器就能访问到你打包好的模块(也就是部署到server上的东西),但一般这个获取访问的过程比较耗费时间,于是就有了缓存这个技术,用来提升加载速度;但是如果我们修改了某个文件内容,没有修改文件的名称,浏览器会认为他没有被更新,就只能使用它的缓存版本,下面有三种方法

第一种方法 在出口文件的名称下配置10位哈希值 hash
hash是项目级别的,它的计算和整个项目的构建有关,固此构建生成的文件hash一样。即每次修改任何一个文件,所有文件名的hash都将改变。就算没有改变里面的内容,hash值也会发生改变;假如修改了任何一个文件,整个项目的文件缓存都将失效。

output: {
    
    
    // 名称
    filename: 'js/built[hash:10].js',
    // 地址
    path: resolve(__dirname, 'build')
},

如果需要也可以在plugins下面给其他文件使用

    plugins: [
    // html文件
    new HtmlWebpackPlugin({
    
    
        template: './src/index.html',
        // html压缩
        minify: {
    
    
            collapseWhitespace: true,
            removeComments: true
        }
    }),
    // css文件使用
    new MiniCssExtractPlugin({
    
    
        filename: 'css/built[hash:10].css'
    }),
],

第二种方法 chunkhash
chunkhash是模块级别的,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的hash值
根据chunk生成hash值,如果打包来源于同一个chunk,那hash值就一样 , js和css还是使用一个hash值 也不行。

output: {
    
    
    // 名称
    filename: 'js/built[chunkhash:10].js',
    // 地址
    path: resolve(__dirname, 'build')
},

第三种方法contenthash
contenthash是文件内容级别的,只有文件的内容变了,contenthash值才改变,该方法有效

output: {
    
    
    // 名称
    filename: 'js/built[contenthash:10].js',
    // 地址
    path: resolve(__dirname, 'build')
},

2.2.2 tree shaking

当我们写模块时有一些文件或者属性我们虽然引入了但是并不需要使用它,但我们打包文件来说也算增大了打包代码的体积
tree shaking解决了这个问题

要求:必须是ES6模块化下 开启production环境

写一个入口文件和一个测试文件如下;测试文件内写两个函数但入口函数只调用一个,另一个不调用,对打包来说不用的那个增加了打包体积

测试文件ceshi.js

export function mul(a, b) {
    
    
    return x * y
}
export function min(a, b) {
    
    
    return a - b;
}

入口文件index.js

import './index.css';
import {
    
     mul } from './ceshii';

function sum(...args) {
    
    
    return args.reduce((p, c) => p + c, 0);
}

console.log(sum(1, 2, 3, 4));
console.log(mul(2, 3));

当你打包完后在生成的文件下查找未引入的函数会发现找不到。系统自动帮我们删除了

还有一种情况;我们不想删除某些文件怎么办
在package.json文件中配置下面属性 xxx代表不想删除的文件

"sideEffects": 【“ *.xxx ”】    // 所有代码没有副作用(都可以进行tree shaking)  可能会把css/@babel/polyfill文件干掉

这也是当我们引入了一些css属性但没使用时;打包后会发现没有生成css文件
这时候修改json文件就可以解决了

"sideEffects": ["*.css"]

2.2.3 code spilt

代码分割:因为有时候我们会打包所有文件到一个文件里,会导致这个文件特别庞大;我们可以选择把那个大文件分割成为小的文件,并行加载会提高运行速度

——单入口文件

情景一:添加下面属性到module.exports里面

// 可以将node-modules中的代码单独打包成一个chunk最终输出  
// 如果是多入口,会自动分析
optimization: {
    
    
    splitChunks: {
    
    
        chunks: 'all'
    }
},

最后生成两个文件(一个是node-modules文件;一个是自己写的文件)

情景二:利用import关键字添加代码到js文件里

import ( /*webpackChunkName:'ceshii'*/ './ceshii')
.then(({
     
      mul, min }) => {
    
     console.log(mul(2, 5)); })
    .catch(() => {
    
     console.log('文件加载失败'); })

利用import引入一个文件;会分割成一个文件;引入多个会生成多个文件

——多入口文件:

entry: {
    
    
    index: './src/index.js',
    ceshi: './src/ceshii.js',
},

完整代码参考

// css处理引入
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    // 输出文件的绝对路径
const {
    
     resolve } = require('path')
    // html文件处理
const HtmlWebpackPlugin = require('html-webpack-plugin')
const PostcssPresetEnv = require('postcss-preset-env')


// 定义nodejs的环境变量  在那个环境下
process.env.NODE_ENV = 'production';
// 提取css兼容性处理的代码为公共代码
const commonCssLoader = [
    MiniCssExtractPlugin.loader,
    'css-loader',
    //修改loader的默认设置
    {
    
    
        loader: 'postcss-loader',
        options: {
    
    
            postcssOptions: {
    
    
                plugins: [
                    PostcssPresetEnv()
                ]
            }
        }
    }
]

module.exports = {
    
    
    // // 入口文件-单入口(输出所有文件都会输处在一个文件里面)
    entry: './src/index.js',
    // 多入口 有一个入口就会有一个bundle
    entry: {
    
    
        index: './src/index.js',
        ceshi: './src/ceshii.js',
    },

    // 文件输出
    output: {
    
    
        // 名称  [name]  取文件名
        filename: 'js/[name].[contenthash:10].js',
        // 地址
        path: resolve(__dirname, 'build')
    },
    plugins: [
        // html文件
        new HtmlWebpackPlugin({
    
    
            template: './src/index.html',
            // html压缩
            minify: {
    
    
                collapseWhitespace: true,
                removeComments: true
            }
        }),
    ],
    // 可以将node-modules中的代码单独打包成一个chunk最终输出
    // 如果是多入口,会自动分析
    optimization: {
    
    
        splitChunks: {
    
    
            chunks: 'all'
        }
    },
    mode: 'production',
}

2.2.4 懒加载和预加载

懒加载  当文件需要时才会被加载
console.log('index.js被加载了');

import {
    
     mul } from './ceshii'
console.log(mul);

document.getElementById('btn').onclick = function() {
    
    
    // 懒加载  当文件需要时才会被加载
    import ( /* webpackChunkName:'ceshii'*/ './ceshii').then(({
     
      mul }) => {
    
    
        console.log(mul(4, 5));
    })

}

这个是ceshi.js

export function mul(a, b) {
    
    
    return a * b
}
export function min(a, b) {
    
    
    return a - b;
}
预加载  在使用前提前加载js文件  等其他资源加载完毕 浏览器空闲的时候 偷偷加载 webpackPrefetch:true开启预加载
console.log('index.js被加载了');

import {
    
     mul } from './ceshii'
console.log(mul);

document.getElementById('btn').onclick = function() {
    
    
    // 懒加载  当文件需要时才会被加载
    // 预加载  在使用前提前加载js文件  等其他资源加载完毕 浏览器空闲的时候 偷偷加载
    // 正常加载可以认为是并行加载(同一时间加载多个文件)
    import ( /* webpackChunkName:'ceshii',webpackPrefetch:true*/ './ceshii').then(({
     
      mul }) => {
    
    
        console.log(mul(4, 5));
    })

}
正常加载可以认为是并行加载(同一时间加载多个文件

2.2.5 PWA

PWA:渐进式网络开发应用 离线可访问

下载插件:
workbox-webpack-plugin
引入插件:

   // 引入插件(离线打包)
const workWebpackPlugin = require('workbox-webpack-plugin')

使用插件:
在plugins里面写如下代码

        new workWebpackPlugin.GenerateSW({
    
    
            // 1. 帮助serviceWWorker快速启动
            // 2. 删除旧的 serviceWorker
            clientsClaim: true,
            skipWaiting: true,
        })

在js里面注册serviceworker

// 注册serviceworker
// 处理兼容性问题
if ('serviceWorker' in navigator) {
    
    
    window.addEventListener('loader', () => {
    
    
        navigator.serviceWorker
            .register('/service-worker.js')
            .then(() => {
    
    
                console.log('sw注册成功了');
            })
            .catch(() => {
    
    
                console.log('sw注册失败了');
            })
    })
}

swd代码必须运行在服务上
下载插件:

  npm i serve -g     // 构建一个服务器

启动服务器

serve -s build   // 将build目录下的所有资源作为静态资源暴露出去

猜你喜欢

转载自blog.csdn.net/weixin_51992868/article/details/124388068
今日推荐