最近我担任的一项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东西来绘制页面的全体链路图,并在合作每个链路项的耗时剖析的基础上,辨认页面功能瓶颈所在。这次优化也是经过链路剖析的方法才干证明数据 + 图片预加载的方法是可行的。但是,功能优化并非一劳永逸,每个页面都有不同的功能优化方案。因而,我在这里仅供给了一种思路,终究的优化策略还需开发人员依据具体情况进行深化思考。