最近我担任的一项B端项目无法运用SSR(可参阅我之前写的文章:我为什么没用上NextJS)。为了提高首屏加载体会,我挑选了另一种优化办法——数据 + 图片预加载。
项目形态
我担任的项目的页面很类似于上图所展示的页面。这个页面的特点是,内容区的图片都归于必要内容,因而在所有图片加载完成后才算作是首屏加载结束。另一个特点是页面的图片尺寸相对较大,因而图片下载时刻较长。但是,由于成本的考量,图片服务供给方并不愿意对图片进行压缩。
能够看到,图片的巨细已经到了 1.2MB,下载时刻大约1秒。
依据实验室数据显示,该页面的初次内容绘制时刻(LCP)大概需求2.4秒。考虑到在正式上线后,页面的首屏时刻预计会超过这个水平,因而优化的空间仍然存在。
链路剖析
上图展示的是运用Chrome Performance 东西剖析的整个页面加载过程。我之前也用过 Performance 东西剖析过剪映的功能,有爱好的同学能够看下。
该页面能够粗略地分为4个阶段:
- HTML加载:页面的HTML内容加载也需求时刻,耗时为200毫秒。
- JS加载:包含前置JS资源、主JS和页面JS的加载。这是典型的自行完成路由的单页运用页面JS加载链路,耗时为900毫秒。
- 后台接口恳求:耗时为300毫秒。
- 列表图片:首屏加载完成需求等待列表图片加载结束,耗时为1000毫秒。
该功能链路的问题在于:图片的下载需求在JS加载和后台接口恳求之后才会履行。事实上,咱们能够将这个串行的链路改为并行的。
优化后的链路
如上图所示,后台接口在所有JS加载之前发起恳求,获取到图片的URL后,预加载这些图片。当烘托进程需求运用这些图片时,图片数据已经准备就绪,能够直接进行烘托。
为什么这种修改能够削减首屏时刻呢?这是由于浏览器拥有两个线程:首要JS线程和网络线程,这两个线程能够并行工作。因而,在首要JS线程运行时,能够尽可能多地运用网络线程来恳求资源,以最大限度地提高浏览器的功能。
尽管后台接口的发起逻辑被放置在所有JS加载之前,但由于JavaScript文件的履行优先级高于XHR恳求,因而会有200毫秒的推迟才会触发后台恳求。
进行链路优化后,实验室中的首屏时刻从原来的2.4秒优化到1.7秒,节约了0.8秒的首屏时刻
。
代码完成
完成图片预加载的首要要点是将后台恳求和图片恳求放到其他JS加载之前。例如有这样一个 index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- 数据与图片预加载逻辑 -->
<script src="./prefetch-data.js"></script>
<!-- 模仿react.js等前置js恳求,这些恳求可能耗时挺久的 -->
<script src="//cdn.js/react.js"></script>
<!-- 主数据逻辑 -->
<script defer src="./index.js"></script>
</body>
</html>
然后是完成预加载恳求逻辑, prefetch-data.js
:
window.prefetchData = {};
window.onPrefetchData = {};
// 模仿预加载一个用户后台接口
const prefetchUserData = async () => {
const apiKey = "userData";
const res = await fetch("/api/getUserList");
// 预加载图片
res.images.forEach((imageUrl) => {
new Image(imageUrl).src = imageUrl;
});
// 放入缓存中
window.prefetchData[apiKey] = res;
// 假如运用方的逻辑履行比 api 早,则调用运用方的数据回调
window.onPrefetchData[apiKey]?.(res);
};
prefetchUserData();
在实际项目中,咱们能够经过构建东西将上述代码合并成一个chunk后插入到html中。本项目运用的是Webpack,在合作Webpack的entry配置和html-webpack-plugin,完成了这个作用。
经过浏览器的 new Image()
的方法,能够预加载图片到缓存中,等有需求的时分再运用。
在主JS获取数据的方法如下所示,index.js
:
const getUserData = () => {
return new Promise((resolve) => {
const apiKey = "userData";
// 假如已经恳求结束,则直接运用
if (window.onPrefetchData[apiKey]) {
resolve(window.data);
return;
}
// 假如还没恳求结束,则在window上挂载一个监听函数
window.onPrefetchData[apiKey] = (data) => {
resolve(data);
};
});
};
const init = async () => {
const data = getUserData();
console.log(data);
};
init();
index.js
需求跨越 JS 获取 prefetch-data.js
的内容,能够经过将变量挂载到window的方法完成数据的交流。
总结
做功能剖析时,我强烈推荐运用Chrome Performance东西来绘制页面的全体链路图,并在合作每个链路项的耗时剖析的基础上,辨认页面功能瓶颈所在。这次优化也是经过链路剖析的方法才干证明数据 + 图片预加载的方法是可行的。但是,功能优化并非一劳永逸,每个页面都有不同的功能优化方案。因而,我在这里仅供给了一种思路,终究的优化策略还需开发人员依据具体情况进行深化思考。