前言
之前在 为什么有人说 vite 快,有人却说 vite 慢? 一文中,我们有提到开发模式下使用 Vite
会有首屏和懒加载性能下降的负面效果,并且在文章的结尾提到了可以通过持久化缓存
、prefetch
的方式来做优化,那今天我们就来试一下其中的一种手段:通过 prefetch
优化懒加载性能。
本文的目录结果如下:
效果展示
我们先给大家展示一下没有做懒加载优化的效果和优化以后的效果:
-
webpack 的效果:
抛开
dev server
的启动速度慢以外,懒加载的性能还是很不错的。 -
Vite 没有做懒加载优化的效果
使用
Vite
以后,dev server
启动速度飞起,但首屏性能、懒加载性能和Webpack
相比还是有很明显的差距。 -
Vite 做懒加载优化的效果
优化以后的
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)
]
}
效果如下:
html
页面结构如下:
观察 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);
]
}
效果如下:
入口文件 /src/index.tsx
的代码如下:
这次优化以后,首屏性能不像上一次那么差,懒加载性能也非常优秀,优化策略生效,😄。但依然存在一个问题,那就是首个懒加载页面展示特别慢。
我们还是简单做一下分析。整个首页的加载过程为:
- 首屏加载;
prefetch
懒加载资源;- 根据当前路由懒加载
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);
]
}
效果如下:
懒加载页面代码如下:
老实说,这并不是一个完美的策略,依旧存在如下问题:
-
timeout
需要开发人员去调整; -
如果用户切换到某个页面,而这个时候如果
dev server
还没有完成prefetch
资源的转换工作,那么要访问的页面可能会花较长的时间才能渲染完成;
不过,方案三比起方案一和方案二来说,依旧是有进步的。该方案最完美的应用场景是首屏渲染完成以后,用户在当前页面停留一段时间,然后再去切换到其他页面。(用户的使用习惯也是这样的吧,😂)。
结束语
到这里,关于 Vite
开发模式下懒加载的性能优化就结束了。
为了优化懒加载性能,小编一共实践了三种技术方案,其中方案三满意度最高。尽管对开发人员的心智负担最高,但效果最好。如果小伙伴们感兴趣,可以自己去试试。另外,如果小伙伴们有更好的方案,可以在留言区告诉小编,小编会马上去尝试的。