漫谈构建工具(六): 我是如何利用 prefetch 来提升 Vite 开发模式下懒加载的性能

21,383 阅读9分钟

前言

之前在 为什么有人说 vite 快,有人却说 vite 慢? 一文中,我们有提到开发模式下使用 Vite 会有首屏和懒加载性能下降的负面效果,并且在文章的结尾提到了可以通过持久化缓存、prefetch 的方式来做优化,那今天我们就来试一下其中的一种手段:通过 prefetch 优化懒加载性能。

本文的目录结果如下:

效果展示

我们先给大家展示一下没有做懒加载优化的效果和优化以后的效果:

  • webpack 的效果:

    Aug-12-2022 17-39-15.gif

    抛开 dev server 的启动速度慢以外,懒加载的性能还是很不错的。

  • Vite 没有做懒加载优化的效果

    Aug-12-2022 17-48-40.gif

    使用 Vite 以后,dev server 启动速度飞起,但首屏性能、懒加载性能和 Webpack 相比还是有很明显的差距。

  • Vite 做懒加载优化的效果

    Aug-12-2022 19-28-35.gif

    优化以后的 Vite 应用,尽管首屏性能还和以前一样,但是懒加载性能有了明显的提升。当我们打开客户管理页面时,页面会很快的完成渲染。

接下来小编就给大家具体讲一讲整个性能优化的过程。

为什么懒加载性能会差

首先我们先来解释一下为什么通过 Vite 启动 dev server 以后,懒加载性能会差(注意:性能差仅限于 dev server 启动以后首次打开应用的某一个页面)。

原因有两点:

  • 大量的 http 请求;

  • dev server 实时对浏览器请求的文件做 transform;

由于 Vite 采取了 unbundle 机制,源文件并没有像 webpack 那样做合并捆绑,这就导致了不管是首屏还是懒加载,都会因为请求 js、css 等资源产生大量的 http 请求。另外,在请求过程中,dev server 需要对源文件做实时转换。其中,实时转换源文件是影响最大的。

这两种情形,和我们常用的优化手段如减少 http 请求、提前对源文件做处理背道而驰,也就导致了懒加载性能下降。

知道了性能下降的原因,那我们就可以针对性的做性能优化了。

http 请求数量多这个问题,影响较小,因此小编只重点关注源文件实时处理这个问题。解决方案也很简单,就是在用户点击某个页面之前,提前向 dev server 发起请求,让 dev server 提前去对请求文件做 transform,等到真正需要加载某个资源时,直接返回已经 transform 的资源。

这种手段也就是我们常说的 prefetch。

通过 prefetch 优化懒加载性能

提到 prefetch,大家第一想到的肯定是常用的 preload、prefetch、preconnect 优化策略。

// 用于提前获取首屏资源
<link ref="preload" href="xxxxx" as="script" />

// 用于提前获取非首屏资源
<link ref="prefetch" href="xxxx"  as="script" />

// 用于提前建立 http 连接
<link ref="preconnect" href="xxx" />

由于需要优化懒加载性能,我们完全可以使用 prefetch 策略,在 html 文件中添加需要 prefetch 的资源,等到浏览器空闲的时候去提前加载资源,让 dev server 提前对请求文件做 transform。这样等切换到某个页面,真正需要获取相关资源时,可以快速 get。

有了方法,那我们就开始行动吧,💪🏻。

使用 prefetch 策略,有两个问题需要解决:

  • 找到项目中需要懒加载的 path;

  • 将 prefetch 链接添加到 html 文件中;

首先是找到项目需要懒加载的 path。要完成这一步,有简单的实现,也有复杂的实现。

简单的方式,就是开发人员手动定义一个需要 prefetch 的懒加载 path 列表。这种方式简单易用,方便快捷,而且使用灵活,但是会加重开发人员的维护成本。

复杂的方式,是通过技术手段实现。这种方式,和预加载需要提前获取项目中所有的第三方依赖一样,通过对项目整个源文件做一个依赖分析,然后识别出其中需要懒加载的 path。因此我们可以像预加载一样,借助 esbuild 实现。但是这种方式有一个很大的问题,那就是由于我们需要把识别的 path 添加到 html 中,导致识别过程必须在浏览器请求 html 之前完成,这就存在阻塞浏览器首屏请求的可能,使得首屏性能下降。

综合两种方式的优劣和实现成本,小编最后决定采用第一种方式。

把 prefetch 链接添加到 html 文件中就非常简单了,我们可以通过 Vite 提供的 transformIndexHtml hook 实现。这个 hook 给我们提供了在 dev server 返回 html 文件之前修改 html 的机会。

所有问题都解决了,那我们就来写一个自定义 Vite plugin 来实现 prefetch 功能吧。

// PrefetchLazyPathsPlugin.ts
export const PrefetchLazyPathsPlugin = (paths: string[] = []) => {
    return {
        name: 'prefetch-lazy-paths-plugin',
        async transformIndexHtml(html: string) {
            if (!paths.length) return html;
            let prefetchStr: string = '';
            paths.forEach((item) => {
                prefetchStr += `<link rel="prefetch" href="${item}" as="script" />`;
            });
            let newHtml = html.replace('</head>', `${preloadStr}</head>`);
            return newHtml;
        },
    }
}

这个 plugin 的用法如下:

// vite.config.ts
const lazyPaths = [
    '/src/pages/order-manage/index.tsx',
    '/src/pages/customer-manage/index.tsx',
    '/src/pages/customer-group/index.tsx',
    ...
]

export default {
    ...
    plugins: [
        PrefetchLazyPathsPlugin(lazyPaths)
    ]
}

效果如下:

Aug-13-2022 21-59-40.gif

html 页面结构如下:

image.png

观察 gif 图中切换路由时页面的渲染速度,可以看到我们的优化策略是生效的,而且效果也非常不错,但是却带来一个很严重的问题,那就是首屏性能更差了。

小编也分析了一下原因,主要是 prefetch 策略使得 dev server 需要在首屏期间需要同时处理首屏资源 + 懒加载页面资源,导致首屏资源的响应速度变慢,使得首屏性能下降。

这样子的性能优化肯定是没有意义的,我们不能牺牲首屏性能去提升懒加载性能。

那有没有更好的 prefech 策略呢?或者说我们能不能在首屏完成以后再去 prefetch 呢?

答案是可以的。我们可以通过 fetch API, 在合适的时机手动调用这个 API 来实现 prefetch

prefetch 二次优化

通过手动调用 fetch API 的方式来实现 prefetch,问题也有两个:

  • 什么时候调用 fetch ?

  • 调用 fetch 代码怎么注入?

什么时候调用 fetch 的问题,我们可以通过 onload 来处理。onload 会在页面所有资源加载完毕以后触发,这样我们就可以在 onload 内部调用 fetch 去实现 prefetch。

而注入 fetch 代码的问题,也好处理。我们可以通过 Vite 提供的 transform hook 实现。这个 hook 给我们提供了在 dev server 返回 transform 以后的代码之前,修改代码的机会。通过这个 hook,我们可以在入口文件 /src/index.tsx 完成 transform 以后、返回浏览器之前注入 fetch 代码。

有了方案,那我们就来写代码实现吧。

// PrefetchLazyPathsPlugin.ts
export const PrefetchLazyPathsPlugin = (entry: string, paths: string[] = []) => {
    return {
        name: 'prefetch-lazy-paths-plugin',
        async transform(code, id) {
            // 只对入口文件做代码注入
            if (id.includes(entry)) {
                return `
                    ${code};
                    window.onload = () => {
                        const lazyPages = ${JSON.stringify(paths)};
                        lazyPages.forEach(item => fetch(item));
                    }
                `
            }
            return code;
        },
    }
}

这个 Plugin 的用法如下:

// vite.config.ts
const lazyPaths = [
    '/src/pages/order-manage/index.tsx',
    '/src/pages/customer-manage/index.tsx',
    '/src/pages/customer-group/index.tsx',
    ...
]

export default {
    ...
    plugins: [
        PrefetchLazyPathsPlugin('/src/index.tsx', lazyPaths);
    ]
}

效果如下:

Aug-14-2022 09-47-55.gif

入口文件 /src/index.tsx 的代码如下:

image.png

这次优化以后,首屏性能不像上一次那么差,懒加载性能也非常优秀,优化策略生效,😄。但依然存在一个问题,那就是首个懒加载页面展示特别慢。

我们还是简单做一下分析。整个首页的加载过程为:

  1. 首屏加载;
  2. prefetch 懒加载资源;
  3. 根据当前路由懒加载 order-manage 页面;

prefetch 导致 dev-server 需要同时处理 order-manage 页面资源 + 其他懒加载页面资源,导致 order-manage 资源响应速度变慢,使得 order-manage 页面渲染速度变慢。

这个问题同样也有解决方案,我们可以在首页懒加载完成以后再去 prefetch。

prefetch 三次优化

首页懒加载完成以后再次 prefetch 有一个问题:我们怎么确定懒加载已完成。

解决这个问题也有两个难点:

  • 如何量化懒加载已经完成 ?

  • 用户首页懒加载的页面无法确定,我们怎么注入 prefetch 代码 ?

针对这个问题,小编目前没有特别好的解决方案,只能通过一个非常笨的方法:

  • 给每个懒加载页面都注入 prefetch 代码;

  • 人为通过 setTimeout 延迟 1000 ms 去 prefetch 来防止首个懒加载页面资源响应速度变慢;

优化以后的代码实现如下:

// PrefetchLazyPathsPlugin.ts
export const PrefetchLazyPathsPlugin = (paths: string[] = [], timeout: number = 1000) => {
    return {
        name: 'prefetch-lazy-paths-plugin',
        async transform(code, id) {
            if (paths.length) {
                for(let path of paths) {
                    if (id.includes(path)) {
                        return `
                            ${code};
                            const lazyPages = ${JSON.stringify(paths)};
                            setTimeout(() => {
                              lazyPages.forEach(item => fetch(item));
                            }, timeout);
                        `
                    }
                }
            }
            return code;
        }
    }
}

这个 Plugin 的用法如下:

// vite.config.ts
const lazyPaths = [
    '/src/pages/order-manage/index.tsx',
    '/src/pages/customer-manage/index.tsx',
    '/src/pages/customer-group/index.tsx',
    ...
];

export default {
    ...
    plugins: [
        PrefetchLazyPathsPlugin(lazyPaths, 1000);
    ]
}

效果如下:

Aug-14-2022 15-25-21.gif

懒加载页面代码如下:

image.png

老实说,这并不是一个完美的策略,依旧存在如下问题:

  • timeout 需要开发人员去调整;

  • 如果用户切换到某个页面,而这个时候如果 dev server 还没有完成 prefetch 资源的转换工作,那么要访问的页面可能会花较长的时间才能渲染完成;

不过,方案三比起方案一和方案二来说,依旧是有进步的。该方案最完美的应用场景是首屏渲染完成以后,用户在当前页面停留一段时间,然后再去切换到其他页面。(用户的使用习惯也是这样的吧,😂)。

结束语

到这里,关于 Vite 开发模式下懒加载的性能优化就结束了。

为了优化懒加载性能,小编一共实践了三种技术方案,其中方案三满意度最高。尽管对开发人员的心智负担最高,但效果最好。如果小伙伴们感兴趣,可以自己去试试。另外,如果小伙伴们有更好的方案,可以在留言区告诉小编,小编会马上去尝试的。