惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
J
Java Code Geeks
有赞技术团队
有赞技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
Apple Machine Learning Research
Apple Machine Learning Research
量子位
D
Docker
V
Visual Studio Blog
博客园 - 司徒正美
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
WordPress大学
WordPress大学
U
Unit 42
M
MIT News - Artificial intelligence
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
G
Google Developers Blog
Engineering at Meta
Engineering at Meta
V
V2EX
大猫的无限游戏
大猫的无限游戏
雷峰网
雷峰网
Vercel News
Vercel News
C
Check Point Blog

博客园 - Liaofy

前端新范式:用 AI 提效开发,用 E2E 保证迭代质量 使用mumu模拟器抓包 andriod app 如何在本地给 vue-router4 扩展功能? Vue3 路由优化,使页面初次渲染效率翻倍 【eslint 插件开发】禁用 location 跳转外部链接 vue 项目使用 charles 代理线上页面到本地后显示404 避免使用单个数字作为对象的key 高扩展弹出层组件设计实现(H5&小程序) 轻松搞懂POST与PUT的区别 vite(or rollup) 实现 webpackChunkName webapp项目架构畅想(vue3).md vite试玩:老项目启动从6分钟到秒开 修剪AST树减少webapck打包尺寸 【uniapp】改善中大型uniapp小程序项目开发体验 vscode 调试 webpack 打包 5分钟,搞清 Unicode 与 UTF-8 的区别 deAsync.js 真正让异步变同步 盘的转——使用缓动函数完成动画 多个请求间断失败,如何优雅处理?
把SPA的加载速度提升一倍,有没有搞头?
Liaofy · 2024-04-29 · via 博客园 - Liaofy

众所周知,纯前端渲染的单页应用(SPA)渲染比较慢,其中一个问题是:先加载公共js,再根据路由动态加载页面需要的js。如下图:

  1. commons.e7c0f857.js加载并解析完毕后(2.53s)
  2. 再发起对order.15cd187e.js的加载(2.24s)。

本文以 VUE 项目为例。

为什么会出现串行加载的情况

诚然,这是由于路由懒加载造成的。但import()说这个锅它不想背,因为动态加载,本来就是在需要的时候才去加载。

把串行加载改造成并行加载

想要加速js加载过程,必须将上述js进行并行加载,这看起来和动态加载十分矛盾。

不过,我们暂且不要考虑那么多,先看看能不能做。

一个常见的路由配置如下:

{
 path: '/domestic/detail',
 component: () =>
   import(
     /* webpackChunkName: "jipiao/search/domestic" */ '../domestic/detail'
   ),
}

要实现文件的并行加载,我们只需要两步操作:

  1. 找到path需要的全部js/css文件,得到一个配置文件
  2. 当请求到达服务器时,将匹配到的文件写入到html后,再返回给浏览器

生成 path 对应的配置文件

如果你用的是webpack进行打包,很容易通过自定义插件,在emit hook得到path对应的js/css依赖文件,最终生成一个配置文件。以下代码仅作参考:


class PreloadChunkPlugin {
  apply(compiler) {
    const webpackChunkNameMap = pathList.reduce((acc, item) => {
      acc[item.webpackChunkName] = {
        path: item.path,
        chunks: []
      }
      return acc
    }, {})
    
    const mode = compiler.options.mode;
    compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
      const chunkDepSet = compilation.chunks.reduce((acc, chunk) => {
        const chunkName = chunk.name
        if (chunkName && webpackChunkNameMap[chunkName]) {
          if (!acc[chunkName]) acc[chunkName] = {
            chunkSet: new Set(),
            chunkIdSet: new Set()
          }
          ;[...chunk._groups].forEach(chunkGroup => {
            chunkGroup.chunks.forEach(subChunk => {
              const hash = mode === 'production' ? subChunk.contentHash.javascript.substring(0, 8) : ''
              acc[chunkName].chunkSet.add(`${(subChunk.name || subChunk.id)}${hash ? '.' + hash : ''}.js`)
              acc[chunkName].chunkIdSet.add(subChunk.id)
            })
          })
        }
        return acc
      }, {})

      const pathMap = {}
      Object.keys(chunkDepSet).forEach(key => {
        const {
          chunkSet,
          chunkIdSet,
        } = chunkDepSet[key]
        pathMap[webpackChunkNameMap[key].path] = {
          chunks: [...chunkSet],
          chunkIds: [...chunkIdSet]
        }
      })

      fs.writeFileSync(
        path.resolve(__dirname, '../../app/assets-path2chunks.json'),
        JSON.stringify(pathMap)
      );

      callback();
    });
}

当然,vite插件也可以在generateBundle中做到。

好了,假设我们已经得到了最终的配置文件:

{
  '/domestic/detail': ['detail.js', 'other.js']
}

动态写入到 html 文件中

我们的项目恰好使用node服务来返回 html,所以比较容易实现,根据请求的路径来匹配对应的依赖文件,然后写入到html中,再返回给服务器。

如果你使用的是nginx,也可以考虑编写lua脚本,实现动态写入的过程。

下图是改造后的效果:

FAQ

Q: 为什么不用多页?

A: 多页应用之间跳转比较慢,应用之间通信也相对麻烦。改造对业务也有侵入。

Q: 为什么不用服务端渲染(ssr)

A: ssr 效果当然更好。但我们项目很大,改造成本过高。新项目当然推荐!