• 前端面试系列之webpack优化


    webpack模块打包机:分析项目结构,找到js模块以及其他 一些浏览器不能直接运行的拓展语言,将其打包成合适的格式以供浏览器使用
    主要做以下的工作:

    • 代码转换:比如说TS编译成js、scss编译成css
    • 文件优化:压缩js、css、html代码,压缩合并图片
    • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载
    • 模块合并:在采用模块化的项目里会有很多的模块和文件,需要构建功能把模块分类合并成一个文件。
    • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器
    • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统

    webpack中的术语

    • Module:module 的角色是资源的映射对象,储存了当前文件所有信息,包含资源的路径、上下文、依赖、内容等
    • Chunk:一些模块 (module) 的封装单元。且直到资源构建阶段都会持续发生变化的代码块,在此期间插件通过各种钩子事件侵入各个编译阶段对 chunk 进行优化处理。可以通过由 SplitChunksPlugin 插件根据规则与 ChunkGraph 对 Chunk 做一系列的变化、拆解、合并操作,重新组织成一批性能更高的 Chunks。后续再为它们做排序和生成hash等一系列优化处理,直到 Compiler.compile 执行完成作为资源输出(emitAssets)。
    • Bundle:是 webpack 进程执行完毕后输出的最终结果。是对 chunk 进行编译压缩打包等处理后的产出。通常与构建完成的 chunk 为一对一的关系。

    总结:Chunk是过程中的代码块,Bundle是结果的代码块。

    如何借助webpack优化前端性能

    关于webpack的性能优化,主要体现在三个方面:
    `构建性能`:是指在开发阶段的构建性能。当构建性能越高,开发效率越高。
    `传输性能`:在这方面重点考虑网络中的总传输量、JS文件数量以及浏览器缓存。
    `运行性能`:主要是指JS代码在浏览器端运行的速度。
    
    • 1
    • 2
    • 3
    • 4

    1构建性能

    是指在开发阶段的构建性能。当构建性能越高,开发效率高越高

    1.1 减少模块解析(模板解析包括:AST抽象语法书分析、依赖分析、模板语法替换,对某个模块不进行解析,可以缩短构建时间)

     如果某个模板不做解析,该模板经过Loader处理后的代码就是最终代码。
     如果没有loader对该模块进行处理,该模块的源码就是最终打包结果的代码。
    
    • 1
    • 2
    module.exports = {
        mode: "development",
        module: {
            noParse: /JQuery/
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.2 限制loader的应用范围
    针对一些第三方库,不使用loader进行处理。例如babel-loader,转换一些本身就是用ES5语法书写的第三方库,反而会浪费构建时间。
    因此通过module.rules.exclude或module.rules.include,排除或仅包含需要应用loader的场景。

    module.exports = {
         module: {
             rules: [
                 {
                     test: /\.js$/,
                     exclude: /node_modules/,
                     //或
                     // include: /src/,
                     use: "babel-loader"
                 }
             ]
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.3 开启多线程打包
    通过thread-loader会开启一个线程池,它会把后续的loader放到线程池的线程中运行,以提高构建效率。
    thread-loader可以通过测试决定放置的位置。

    module.exports = {
        module: {
            rules: [{
                test: /\.js$/,
                use: [
                    "thread-loader",
                    "babel-loader"
                ]
            }]
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    1.6 热替换

    let webpack = require('webpack')
        module.exports = {
            devServer: {
                open: true,
                hot: true  //开启HMR
            },
            module: {
                rules: [{
                    test: /\.css$/,
                    use: ["style-loader", "css-loader"]
                }]
            },
            plugins: [
                  new webpack.HotModuleReplacementPlugin(),
            ]
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    module.hot.accept()的作用是让webpack-dev-server通过socket管道,把服务器更新的内容发送到浏览器,然后,将结果交给插件HotModuleReplacementPlugin注入的代码执行插件HotModuleReplacementPlugin会根据覆盖原始代码,然后让代码重新执行。

    2 传输性能

    重点考虑网络中的总传输量、JS文件数量以及浏览器缓存。

    2.1 分包

    chunk的默认分包规则:
    1. 同一个entry入口模块与他的同步依赖组织成一个chunk
    2. 每一个异步模块与他的同步依赖单独组成一个chunk。其中只会包含入口chunk中不存在的同步依赖;若存在同步第三方包,也会被单独打包成一个chunk。
    
    • 1
    • 2
    • 3

    参考博客
    buildChunkGraph(是 chunk 生成阶段)的三个子方法按顺序来详解:
    1.visitModules

    • 遍历compilation.modules建立起基本的 Module Graph (模块依赖图),为遍历异步依赖(block)等所用。
    • 先处理入口 chunk 的所有同步依赖,遍历时优先将同步依赖嵌套的同步模块添加完再去处理平级的同步依赖。
    • 然后按每个异步依赖的父模块被处理的顺序依次生成异步 chunk 和 chunkGroup。
    • 然后遍历 module graph,为入口模块和它所有(直接/间接)同步依赖形成一个 EntryPoint(继承自 ChunkGroup),入口 chunk此时才会建立起与其依赖模块的联系。
    • 为所有异步模块和它的同步依赖生成一个 chunk 和 chunkGroup(会重复)。如 chunk 的同步模块已存在于入口 chunk,则不会再存入它的_modules中。此阶段初始生成了 chunk graph(chunk 依赖图)。

    2.connectChunkGroups

    • 检查入口 chunk 和 有异步依赖的异步 chunk, 如果它们的子 chunk 有它们未包含的新模块,就建立它们各自所属 chunkGroup 的 父子关系。

    3.cleanupUnconnectedGroups

    • 找到没有父 chunkgroup 的 chunkgroup,删除它里面的 chunk,并解除与相关 module、chunk、chunkGroup 的关系。

    可以使用splitChunks来指定规则,自动分包,下面讲一下splitChunks的用法:

    splitChunks主要作用是提取公共代码,防止代码被重复打包,拆分过大的js文件,合并零散的js文件。

    • chunks 选项,决定要提取哪些模块
      默认是 async :只提取异步加载的模块出来打包到一个文件中。
      异步加载的模块:通过 import(‘xxx’) 或 require([‘xxx’],() =>{}) 加载的模块。
    • initial:提取同步加载和异步加载模块;
      如果 xxx 在项目中异步加载了,也同步加载了,那么 xxx 这个模块会被提取两次,分别打包到不同的文件中。
      同步加载的模块:通过 import xxx 或 require(‘xxx’) 加载的模块。
    • all:不管异步加载还是同步加载的模块都提取出来,打包到一个文件中;
    • minSize 选项:规定被提取的模块在压缩前的大小最小值,单位为字节;
      默认为30000,只有超过了30000字节才会被提取。
    • maxSize 选项:把提取出来的模块打包生成的文件大小不能超过maxSize值;
      如果超过了,要对其进行分割并打包生成新的文件。
      单位为字节,默认为0,表示不限制大小。
    • minChunks 选项:表示要被提取的模块最小被引用次数,引用次数超过或等于minChunks值,才能被提取。
    • maxAsyncRequests 选项:最大的按需(异步)加载次数,默认为 6;
    • maxInitialRequests 选项:打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件),默认为4。
    • 优先级 :maxInitialRequests / maxAsyncRequests < maxSize < minSize;
    • automaticNameDelimiter 选项:打包生成的js文件名的分割符,默认为:~
    • name选项:打包生成 js 文件的名称;
    • cacheGroups 选项,核心重点,配置提取模块的方案,里面每一项代表一个提取模块的方案。下面是 cacheGroups 每项中特有的选项,其余选项和外面一致,若 cacheGroups 每项中有,就按配置的,没有就使用外面配置的;
      - test 选项:用来匹配要提取的模块的资源路径或名称,值是正则或函数;
      - priority 选项:方案的优先级,值越大表示提取模块时优先采用此方案,默认值为0;
      - reuseExistingChunk 选项:true / false。
      为true时,如果当前要提取的模块,在已经在打包生成的js文件中存在,则将重用该模块,而不是把当前要提取的模块打包生成新的 js 文件。
      - enforce选项:true / false。
      为true时,忽略minSize,minChunks,maxAsyncRequests和maxInitialRequests外面选项。下面这代码是vue-admin(github有公开代码)后台管理系统中的分块代码。
     config
        .optimization.splitChunks({
          // 针对哪些chunks进行优化,all(对所有chunk应用分包策略)
          // 比如说element-ui也属于基础类库,配置中如果不设置{chunk-ui}.priority的话,会被切分到chunk-libs中
          chunks: 'all',
          cacheGroups: {
          //会将node-modules文件夹下的模块打包进chunk-libs文件夹下。
            libs: {
              name: 'chunk-libs',
              test: /[\\/]node_modules[\\/]/,
              priority: 10,//进入分组的优先级
              chunks: 'initial' // only package third parties that are initially dependent
            },
            elementUI: {
              name: 'chunk-elementUI', // 将elementUI拆分为单个包
              priority: 20, // 权重需要大于libs和app,否则将打包到libs或app中
              //用正则表达式获取node_modules中的element-ui文件夹
              test: /[\\/]node_modules[\\/]_?element-ui(.*)/
            },
            //将src下的components组件打包进chunk-commons文件夹下,且引用超过三次的模块,需要单独拆分。
            commons: {
              name: 'chunk-commons',
              test: resolve('src/components'), 
              // 拆分前必须共享模板的最小 chunks 数。默认值为1
              // 这里主要是判断是否有多个 chunk 共享(引用)了这个 chunk,对于公共组件来说如果只有极少的 chunk 去引用,其实也是不需要进行单独拆分
              minChunks: 3,
              priority: 5,
              //遇到重复报直接引用,不用重新打包
              reuseExistingChunk: true
            }
          }
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    2.3 代码压缩

    module.exports = {
    	optimization:{
    		minimize:true, //是否启动压缩,默认是只在生产环境自动开启。
    		minimizer:[
    			//压缩时使用的插件,当你自动改压缩配置时必须配置相关压缩插件
    			new TerserPlugin(),//js压缩插件
    			new OptimizeCSSAssetsPlugin()//css压缩插件
    		]
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.6 gzip

    const CompressionWebpackPlugin = require('compression-webpack-plugin')
    
    module.export = {
    	plugin:[
    		new CompressionWebpackPlugin({
    			test:/\.js$/ //针对需要预压缩的文件
    			minRatio:0.5 //压缩比率
    		})
    	]
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. 运行性能:主要指JS代码在浏览器端运行时的速度。

    如何查看webpack的打包性能

    在用这两个插件之前都需要先用npm进行下载
    1、采用webpack-bundle-analyzer插件
    可以看到 bundle 的大小以及具体的 code splitting 之后的分包情况,以便于我们对 bundle 进行量化分析。

    const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
    .......
    chainWebpack(config) {
        config.plugin('webpack-bundle-analyzer').use(BundleAnalyzerPlugin)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    下面的是我的一个vue项目打包后的一个性能分析。后台运行:npm run build
    在这里插入图片描述
    2、speed-measure-webpack-plugin(可以查看文件打包速度)

    const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
    const smp = new SpeedMeasurePlugin({
      outputFormat: 'human'
    })
    configureWebpack: smp.wrap({
        // ...
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    后台运行:npm run build
    在这里插入图片描述

    vue-admin中webpack的优化介绍

    vue-admin没有了webpack.config.js文件,是直接在vue.config.js里的chainWebpack方法直接配置,这样做法的好处是用户既可以保留webpack的默认配置,又可以通过chainWebpack设置更具针对性的参数,链式的写法正是避免了用户直接面对webpack多而复杂的配置项。
    当用npm run build进行打包的时候,事实上是根据cacheGroups的默认配置进来分包并包的。
    
    • 1
    • 2

    1 分包策略的优化
    在默认策略的基础上,进行了优化。针对体积大小、公用率、更新频率进行重新划分包。

    • 基础类库chunk-libs:主要放一些vue全家桶
    • 组件库:放element-ui
    • chunk-commen:放置全局组件

    2 Preload / Prefetch
    Prefetch是一种浏览器机制,浏览在下载或者预取用户可能要访问的文档。具体来说是通过 来实现预提取,设置 prefetch 的影响就是会降低首屏打开的速度,因为浏览器去需要加载这个资源。在 webpack4 中是默认开启 preferch 的,在首屏 index.html中会把十几个页面路由文件,一口气下载下来,可以看到打包后的 index.html中加载了非常多没有意义的 js 和 css 文件。
    为了提升首屏打开速度,一般会关闭 prefetch 这个功能,这样首屏只会记载当前页面路由的组件。当然如果真的有大概率即将被访问的资源也可以使用 prefetch 配置来提升性能和体验。

    module.exports = { 
      chainWebpack(config) { 
        // ...
        
        // when there are many pages, it will cause too many meaningless requests
        config.plugins.delete('prefetch')
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Preload也是使用来实现预下载.

    • preload用来声明当前页面的关键资源,强制浏览器尽快加载;
    • prefetch用来声明将来可能用到的资源,在浏览器空闲时进行加载
     config.plugins.delete('prefetch')
     config.plugin('preload').tap(() => [
          {
            rel: 'preload',
            //限制比如 runtime 等文件可以不用预下载的文件,以提升首屏打开速度。
            fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
            include: 'initial'
          }
        ])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    总结:

    • 在默认策略的基础上,进行了优化。将一些vue全家桶、全局组件、Element-UI单独分包。
    • 合理使用Proload以及Prefetch,关闭了Prefetch,提高了首屏打开的速度。针对Proload则,规定某些文件除外,不需要在首屏打开时预加载。
  • 相关阅读:
    Python 每天定时加密压缩指定文件夹
    2023年海南省职业院校技能大赛(高职组)“软件测试”赛项竞赛规程
    一文解码语言模型:语言模型的原理、实战与评估
    JAVA复习【11】单列集合Collection:ArrayList、 LinkedList、HashSet、TreeSet学习与使用
    效率提升利器:一个在线的.NET源码查询网站
    DALL E2【论文阅读】
    Linux之shell脚本初始
    【OpenGL】六、深度测试和模板测试
    WEB日志框架综述
    后端通过@jsonformat格式化数据转发,前端无法正确显示
  • 原文地址:https://blog.csdn.net/qq_40695234/article/details/127617967