前端性能优化之webpack打包优化

2023-12-18 08:10:16 浏览数 (3)

前端工程化彻底盛行的今天,我们已经习惯使用打包工具来帮助我们打包代码到最终能在浏览器运行的js或者css代码,这样我们就可以在编写代码时放心地使用所有的高级语法,其中最让前端coder感到爽快的就是 import export,我们不再需要像以前一样在html里面放很多很多script。或者使用amd。cmd,requirejs工具来写模块引用的代码,这些方便,也让我们很容易忽略一个问题,就是打包的产物的大小,当一个项目足够大时,我们的js甚至可以达到几MB到几十MB,所以,今天就来总结下关于减小构建产物体积,来达到减少首屏加载时间的内容

webpack 官方自带的优化策略 https://www.webpackjs.com/configuration/optimization/

这里以react项目为例,列举需要优化的构建项

一、使用代码拆分,让我们的页面代码构建到单独的js,首次访问页面的时候才加载这块js

代码语言:javascript复制
module.exports = {
  
  optimization: {
    {
      usedExports: true,
      concatenateModules: false,
      chunkIds: 'deterministic',
      runtimeChunk: true, // 将运行时依赖单独打包-运行时依赖如我们使用的async await语法所需的降级兼容代码 设置为 'single' 则所有的runtime依赖打包到一个文件
      // 使用代码拆分 参考文档 https://www.51cto.com/article/689344.html
      splitChunks: {
        chunks: 'async', // webpack 打包chunk分为 entry chunk 和async chunk两种,配置文件中的entry配置的主包是默认拆分的,多个入口,多个 main chunk。async chunk就是使用import('./xxx.js') 一步模块加载方法加载的模块。那么 chunks选项就是指定这两种chunk哪些需要分包的,`initial` 只分包主包, async 只分包异步加载的包。all 分包上面两种包,这里要注意的就是all有时候会理解成“所有”就会以为所有使用了import './xxx.js'引入的包都会被分包
        minSize: 20, // 超过了这个大小的包才会被拆分
        minRemainingSize: 0,
        minChunks: 1, // 被引用次数大于这个数的包才会被拆分,这里要注意的是,被引用是只命中entry chunk 和 async chunk 的引用者才算
        maxAsyncRequests: 30, 
        maxInitialRequests: 30,
        enforceSizeThreshold: 100, // 超过这个大小的包,不管有没有命中上面的配置,都分包
        // 对指定规则的文件使用特定的分包策略
        cacheGroups: {
          vendors: { 
            test: /[\/]node_modules[\/]/, // 匹配文件路径
            type:/.json$/, // 匹配文件类型
            idHint:'vendors',// 用于设置 Chunk ID,它还会被追加到最终产物文件名中,例如 idHint = 'vendors' 时,输出产物文件名形如 vendors-xxx-xxx.js
            minChunks: 1, 
            minSize: 0,
            priority: 2 // 设置优先级,如果文件命中多个groups策略,优先使用这个配置数字较大的规则组
          }
        }
      }
    }
  }
}

接下来,在react路由里,将组件引入代码 import Xxxx from '@src/routes/Xxxx' 修改为如下引用方式

代码语言:javascript复制
//该组件是动态加载的 千万注意,因为组件是动态加载的,那么。就有可能出现加载失败或者加载错误的情况,所以需要使用 Suspense 组件来包裹,组件还未加载,显示fallback中的内容,组件加载完成,显示组件,加载失败会throw一个error,防止页面崩溃
const Home = React.lazy(() => import('./Home'));

function Layout() {
  return (
    // 显示  组件直至 Home 加载完成
    }>
      
        
      
    
  );
}

上面的分包策略的理解注释中的内容提到了分包的条件和规则,那么,为了尽可能减小我们的主包的大小,我们就要尽可能减少在我们的 entry 选项中指定的入口文件中对其他模块的引用,或者使用异步模块引用的方式,常见的几个优化项目为

优化使用到的工具的引用,将必要的工具引用单独提到一个文件中,避免打包其他没用到的代码到主包

有些应用初始化相关但是跟主应用无关的代码,使用异步模块加载,如下

代码语言:javascript复制
// app.ts
(async () => {
const {default: AppInit} = await import('./app-init');
aegis = AppInit.tam();
AppInit.dataInsight();
AppInit.chunkError();
})();

如果在入口文件中有react或者vue路由使用的组件,使用react或vue提供的异步路由方法引入使用

二、将三方库通过CDN引入而不打包到我们的代码包

默认情况下,我们一般都会将我们所需要的依赖,例如react,moment,axios等三方包通过npm或yarn安装到本地,然后直接import进来使用,这种方式势必就会将这些第三方包打包到我们自己的js中,且因为这些库本身体积就较大,所以会导致我们打包出来的js非常大,而且,当我们使用了chunk切分后,各个chunk都会单独打包进去这些依赖内容。针对这种情况,webpack提供了 externals 选项来让我们可以从外部获取这些扩展依赖,

首先,我们需要通过script标签的形式来引入我们需要使用的三方库,有两种方式,一种是手动在 html-webpack-plugin 的html模板文件或者content内容中加入script标签,第二种是使用html-webpack-tags-plugin插件,通过配置的方式往html内容中动态插入script标签,这里推荐后者,原因是方便写判断逻辑,而不是在html中通过ejs模板语法来写判断逻辑

然后,配置externals选项告诉webpack当我们使用import语句导入模块时,实际使用的是是什么内容(一般三方库都会导出一个包含了所有他包含内容的全局变量)

代码语言:javascript复制
const assetsPath = 'https://static.xxx.com/js';
module.exports = {
  externals: isDev ? {} : {
    // 排除不打包
    'react': 'React',
    'react-dom': 'ReactDOM',
    'react-router': 'ReactRouter',
    'react-router-dom': 'ReactRouterDOM',
    'axios': 'axios',
    'moment': 'moment',
    'moment-timezone': 'moment',
    'lodash': '_',
  },
  plugins: [
    ...config.plugins,
    new webpack.ContextReplacementPlugin(/moment[/\]locale$/, /zh-cn|ja|ko/),
    new webpack.DefinePlugin(envKeys),
    // 开发环境不使用这种方式,因为会影响本地开发的热更新
    new HtmlWebpackTagsPlugin({
      tags: isDev ? [] : [
        {
          type: 'js',
          path: '/react-16.11.0.production.min.js',
          attributes: { defer: 'defer' }, // defer: load完成后不立即执行,等带页面DOMLoaded事件执行前执行,等价于把script放到所有dom之后
          publicPath: assetsPath,
          append: false,
        },
        {
          type: 'js',
          path: '/react-dom-16.11.0.production.min.js',
          attributes: { defer: 'defer' },
          publicPath: assetsPath,
          append: false,
        },
        {
          type: 'js',
          path: '/react-router-5.2.1.min.js ',
          attributes: { defer: 'defer' },
          publicPath: assetsPath,
          append: false,
        }, 
        {
          type: 'js',
          path: '/react-router-dom-5.2.1.min.js',
          attributes: { defer: 'defer' },
          publicPath: assetsPath,
          append: false,
        }, 
        {
          type: 'js',
          path: '/axios-0.26.0.min.js',
          attributes: { defer: 'defer' },
          publicPath: assetsPath,
          append: false,
        },
        {
          type: 'js',
          path: '/moment.min.js',
          attributes: { defer: 'defer' },
          publicPath: assetsPath,
          append: false,
        },
        {
          type: 'js',
          path: '/lodash-4.17.21.min.js ',
          attributes: { defer: 'defer' },
          publicPath: assetsPath,
          append: false,
        },
        {
          type: 'js',
          path: '/moment-timezone-with-data-10-year-range.min.js',
          attributes: { defer: 'defer' },
          publicPath: assetsPath,
          append: true,
        },
      ],
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: 'public',
          globOptions: {
            ignore: ['**/index.html'],
          },
          to: 'dist',
        },
      ],
    }),
  ].concat(!isDev ? [new BundleAnalyzerPlugin({analyzerPort: 8889, analyzerMode: 'static'})] : []),
}

0 人点赞