Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wuyunqiang opened this issue Jul 5, 2024 · 0 comments
Open

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

wuyunqiang opened this issue Jul 5, 2024 · 0 comments

Comments

@wuyunqiang
Copy link
Owner

先有问题再有答案

  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 以便调试。

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

系列文章

性能优化合集

参考

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import

https://webpack.js.org/concepts/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant