性能优化之bundle资源优化六大方案

505 阅读9分钟

先有问题再有答案

  1. bundle是什么 为什么需要打包?
  2. 资源体积对首屏性能有什么影响?
  3. bundle资源越小 性能越好嘛?
  4. 如何优化资源体积?
  5. 性能优化还有哪些方案?

打包&bundle

截屏2024-06-25 下午8.20.33.png

为什么要打包:

转译(Transpiling) :开发中经常使用一些新的语言特性,这些语法浏览器可能并不支持,打包的过程中,通常会使用Babel等工具把新的JavaScript语法转译成大多数浏览器能够理解的语法,确保代码在大部分环境中都能正常运行。

资源整合:打包工具会把CSS、图片资源等进行整合,打包成一个或者多个文件,减少HTTP请求的数量。

性能优化:打包工具在打包的过程中,会进行很多优化,例如代码压缩、删除无用代码(Tree Shaking)、代码分割等,这些都可以极大地减小Bundle的体积,提高页面的加载速度。

bundle

在Web开发中,我们编写的多个JavaScript文件,经过打包工具像Webpack、Rollup处理后,最终形成一个或多个包含了所有JavaScript代码的文件,这个文件就是Bundle

优化思路

截屏2024-06-25 下午7.38.04.png

网络下载

bundle的体积越大,需要下载的数据就越多,这就需要更多的时间和更大的带宽。相应地,如果Web bundle的体积越小,需要下载的数据就越少,那么页面的加载速度就更快,用户的等待时间也就更短。

在HTTP/1.1协议下, 因为每一个额外的JavaScript文件都需要额外的HTTP请求,这会产生额外的延迟。如果拆分得过细,会导致大量的HTTP请求,这也会影响加载性能。

在HTTP/2中,由于支持了多路复用,可以同时传输多个请求或响应,而不用等待上一次请求或响应完成。这一点让文件的拆分和组织更有优势,因为不需要像HTTP/1.1那样担心多个文件会造成多个HTTP请求带来过多的延迟。
但是,这并不表示bundle就应该越小越好,因为仍然存在一些因素需要考虑:

  1. 资源中的代码重复:如果bundle分割得过细,可能会导致一些重复的代码被多次下载和执行,尤其是如果几个bundle中都使用到了同一部分的公共代码。
  2. 浏览器解析和编译开销:每一个额外的bundle都需要浏览器进行单独的解析和编译,如果bundle拆分得过细,各个小的bundle的解析、编译开销加起来可能会比一个相对大一些的bundle要大。

解析执行

JavaScript文件需要被浏览器解析和执行,这是一个需要消耗时间的过程。JavaScript文件越大,解析和执行的时间就越长。反之,如果JavaScript文件体积越小,解析和执行就会更快。

JavaScript解析执行除了和包体积有关,还和具体的业务代码有关系,如果有同步执行的耗时任务,例如在首屏的代码中 执行了1000w次循环计算,因为js单线程的原因,那么必然会导致js执行时间过长,进而导致页面首屏变慢。

关于这部分的优化方案可以参考 浏览器:帧&事件循环 & js性能优化:时间切片分帧,webworker并行, requestidlecallback空闲执行,延迟执行~ 这里不做讨论。

总结

对bundle资源的优化 收益主要来源于下载解析执行这两个关键节点,我们需要综合考虑,保持bundle的大小适中,尽量避免过大或过小的bundle。找到 平衡点 达到 收益最大化.

一图胜千文

截屏2024-06-27 上午10.43.53.png

代码分割(Code Splitting)

介绍

通过将代码分割成多个较小的包,只加载用户当前所需要的功能代码,减少了首屏需要加载的JavaScript代码量,从而降低了首屏的加载时间。

应用

项目打包一般是使用webpack来做的, 想在项目中使用代码分割的功能,就不得不提到webpack的 split-chunks-plugin 插件。

  splitChunks: {
      cacheGroups: {
        defaultVendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    },

上面是vue2默认的 split-chunks-plugin配置项。 简单分析下:

  • defaultVendors: 这是一个特定的缓存组,用于处理来自 node_modules 目录的第三方库模块。

    • name: 指定生成的 chunk 名称。在这个例子中,所有被这个缓存组匹配的模块将被打包到名为 chunk-vendors.js 的文件中。
    • test: 使用正则表达式来匹配应该被这个缓存组包含的模块路径。这里 [\/]node_modules[\/] 是一个正则表达式,用于匹配所有在 node_modules 目录下的文件路径。
    • priority: 缓存组的优先级。数字越小,优先级越高。在这个例子中,-10 表示 defaultVendors 缓存组的优先级较低,这意味着如果有多个缓存组规则匹配同一个模块,将优先考虑其他优先级更高的缓存组。
    • chunks: 指定这个缓存组应该应用到哪种类型的 chunks 上。这里 'initial' 表示只对初始 chunks 进行分割,不包括按需加载的 chunks。
  • common: 这是另一个缓存组,用于处理应用程序中共享的模块。

    • name: 指定生成的 chunk 名称。这里所有被这个缓存组匹配的模块将被打包到名为 chunk-common.js 的文件中。
    • minChunks: 表示模块至少需要被多少个 chunks 引用才会被分割出来。在这个例子中,2 表示一个模块至少需要被两个 chunks 共享才会被提取到 chunk-common.js
    • priority: 缓存组的优先级。-20 表示 common 缓存组的优先级更低,如果有多个缓存组规则匹配同一个模块,common 将不会是首选。
    • chunks: 同样指定应用到初始 chunks 上。
    • reuseExistingChunk: 当设置为 true 时,如果一个模块已经被分割到一个 chunk 中,并且这个 chunk 符合当前缓存组的规则,那么 webpack 将重用这个 chunk 而不是创建一个新的 chunk。

经过上面的配置vue2在打包后可能会输出三个文件,业务的main.js,node_module里的chunk-vendors.js,共享的chunk-common.js.

缓存&hash

一般通过contenthash作为文件名的一部分,做资源缓存的依据。

output: {
    path: '/dist',
    filename: 'js/[name].[contenthash:8].js',
    publicPath: '/',
    chunkFilename: 'js/[name].[contenthash:8].js'
}

通过hash + http协议Cache-Control共同完成。

摇树(Tree Shaking)

介绍

通过在编译时根据ast静态分析死区代码,删除项目中未引用的代码,进一步减小了打包后的代码体积,降低了首屏的加载时间。

应用

Tree Shaking生效的条件:

  1. 使用 ES6 模块语法:Tree Shaking只能用于ES6的模块语法,包括 import 和 export。因为这种静态的模块结构可以在编译阶段确定哪些模块会被使用,哪些不会。这让Tree Shaking成为可能。而像CommonJS那样的动态模块系统就无法进行Tree Shaking。
  2. 关闭模块转换功能:如果你使用了Babel这样的编译工具,需要确保关闭了它们的模块转换功能。否则,Babel可能会把你的ES6模块语法转换为无法进行Tree Shaking的CommonJS模块。
  3. 在生产模式下打包:Webpack默认只在生产模式下进行Tree Shaking,因此在进行打包操作时需要设置mode为production。这样做的原因是,开发模式下更关注构建速度和调试,而在生产模式下,Webpack会使用额外的插件,例如UglifyJS,TerserPlugin来摇掉那些没有被引用的代码, 所以实际上提供shake功能的是压缩插件
  4. 设置"sideEffects" :标识代码没有副作用,帮助编译器更高效的删除无用代码,即使不加也可以达到tree shake的功能 只是这个属性对tree shake 有更好的提效。不过需要注意,如果项目中有引入且执行的模块本身就是副作用(比如一些polyfill),那么需要在sideEffects中将其排除在外,以防止被错误地去除。

按需引入

Tree Shaking本身就是一种按需引入方式,但是是基于es6的模块方案来实现的。对于有些库会将代码打包为es5的老代码以支持更多的低版本。对于这种情况 还有另一种方式。
截屏2024-06-26 下午7.02.19.png

es5可以通过将文件打包成多个独立文件,然后在使用时指定具体的模块,以达到按需引入的作用。

单文件 + 相对路径 + babel-plugin-component(路径转换)

动态引入(Dynamic Imports)

介绍:

通过按需加载的方式,只有当特定功能被用到的时候才去加载对应的模块,这样也能有效减小首屏加载的代码量,提升了首屏的加载速度。

应用

const router = new Router({
  mode: 'hash',
  routes: [
    {
      path: '/',
      name: 'main',
      component: Main,
    },
    {
      path: '/list',
      name: 'list',
      component: () => import(/* webpackChunkName:"Dynamic-test1" */ '../../component/index.vue'),
    },
  ],
});

代码压缩(minification)

minimizer: [
      {
        options: {
          test: /\.m?js(\?.*)?$/i,
          chunkFilter: () => true,
          warningsFilter: () => true,
          extractComments: false,
          sourceMap: true,
          cache: true,
          cacheKeys: defaultCacheKeys => defaultCacheKeys,
          parallel: true,
          include: undefined,
          exclude: undefined,
          minify: undefined,
          terserOptions: {
            compress: {
              arrows: false,
              collapse_vars: false,
              comparisons: false,
              computed_props: false,
              hoist_funs: false,
              hoist_props: false,
              hoist_vars: false,
              inline: false,
              loops: false,
              negate_iife: false,
              properties: false,
              reduce_funcs: false,
              reduce_vars: false,
              switches: false,
              toplevel: false,
              typeofs: false,
              booleans: true,
              if_return: true,
              sequences: true,
              unused: true,
              conditionals: true,
              dead_code: true,
              evaluate: true,
              drop_console: true,
              drop_debugger: true
            },
            mangle: {
              safari10: true
            }
          }
        }
      }
    ]

使用 webpack 内置的压缩工具来压缩所有的 JavaScript 文件,启用了多线程和缓存来提高压缩效率,同时生成 source map 以便调试。

压缩选项中启用了一些基本的压缩策略,同时,配置中删除了控制台日志和调试器代码,以减少最终打包文件的大小。

系列文章

性能优化合集

参考

developer.mozilla.org/en-US/docs/…

webpack.js.org/concepts/