先看页面整体加载效果
Web 的性能优化有很多方法论可以来讨论,这里我先介绍一下飞天服务平台首页的业务背景,以及在业务过程中做的有针对性的优化方法。
最近在做飞天服务平台首页的过程中,遇到的页面打开性能有问题。由于飞天服务平台首页的业务特性:用户可以去配置业务模块到首页,每一个模块都可以去做二级的下钻、抽屉打开去做明细的分析,所以往往一个简单的业务指标呈现,背后带着很多业务数据的二次分析,需要二级页面或者抽屉来呈现这些业务洞察。同时用户可以在首页自定义页面的配置,如果用户配置了 10+ 卡片到首页之后,页面打开的时候同时去加载卡片,对于性能压力有很大的考验。
针对飞天服务平台的业务场景,以及之前性能优化的实践,首先我们会从尽可能缩小资源文件入手,让页面加载尽可能快。解决了资源文件文件问题之后,快速让页面可响应,让用户在体感上更加友好成了最重要的事情,所以启动了一系列关键链路优先、非关键链路渐进式加载的优化项,在做完这些优化之后,性能问题已经基本可以得到解决。那有没有更进一步的优化呢,会在第三部分充分利用缓存-用空间换时间,讲述如何使用缓存把性能优化做到极致的过程。资源尽可能小,关键链路优先、非关键链路渐进式加载,充分利用缓存、用空间换时间三个方面也是一个渐进式的性能优化过程。
尽可能小
优化前后的资源树形图对比
优化前的树形图
优化后的树形图
通过优化打包的方式,将静态的加载资源做到尽可能小,同时第三方库单独打包。图示为优化前 vendors 资源体积大小从 1.42 MB 优化到 384 KB。
资源加载优化的方法有很多,优化的思路也比较固定,这里就不再详细讲述,大致列举几种方式:
-
将公共库单独打包
-
按路由对代码进行代码拆分
-
antd 主题和字体文件单独加载
依赖包 Treeshaking 的优化
核心是在 package.json 内,对于 sideEffects 树形进行配置,如果没有副作用直接降 sideEffects 置为 false。
json
"sideEffects": [
"*.less" // 如果没有副作用,直接置为 false 即可
],
将依赖的包做了 treeshaking 之后,就可以做到每个页面、卡片独立打包,互不影响,在最大限度上减少了多余代码的打入,包体积也可以进一步减少。
抽屉组件单独打包
由于业务的特殊性和抽屉交互形式的引入,飞天服务平台首页引入了很多业务实体的代码。抽屉组件很多代码引入就成了一个负担,一方面加大了资源的打包体积,一方面在页面打开的时候执行相关代码,导致页面可交互时间延长,经常需要等待页面渲染完成之后,页面才可以完全可交互。
这里的解决方法是:利用 Umi 的 dynamic 动态加载特性,分文件去加载第三方组件,同时添加一个加载的骨架屏。这样得到的效果是会在打开抽屉的时候,有一个短暂的骨架屏占位,之后渲染出内容。
javascript
/**
* @author linhuiw
* @description 异步加载组件 ProjectDetailCard
*/
import { dynamic } from 'umi';
import { Skeleton } from 'antd';
const ProjectDetailCard = dynamic({
loading: () => {
return (
<Skeleton
active={true}
paragraph={{
rows: 10,
}}
/>);
},
async loader() {
const { ProjectDetailCard } = await import(
/* webpackChunkName: "ProjectDetailCard" */ './index'
);
return ProjectDetailCard;
},
});
export { ProjectDetailCard };
关键链路优先、非关键链路渐进式加载
图片懒加载
arduino
<img src="image.png" loading="lazy" alt="..." width="200" height="200">
当给 img 标签设置了 loading="lazy"属性后,图片会延迟加载,直到资源与视口达到计算出的距离为止。
目前浏览器本身就支持图片的异步加载,是一个性价比很高的优化点。
利用浏览器的空闲,优化渲染过程中不可交互的问题
由于首页卡片内有很多需要交互触发的抽屉组件,导致了在页面打开的时候,会有短暂的页面渲染不可交互的时间。由于页面大量 Dom 渲染,会有 100ms 左右的不可响应时间,是一个非常不友好的体验。
这里充分利用 window.requestIdleCallback的特性,window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
这里主要做的优化是将弹窗的组件在浏览器空闲或者有触发的时候再去渲染,这样可以分阶段的去渲染主要展示内容。
附上分步渲染组件的示例:
javascript
import React, { useState, ReactNode, useEffect } from 'react';
// 判断是在浏览器环境还是在 Node 环境
import isNode from 'detect-node';
export const IdleUntilUrgent = ({ children, htmlElement }) => {
const [callbackId, setCallbackId] = useState(null);
const [renderChild, setRenderChild] = useState(false);
useEffect(() => {
if (!isNode) {
if (typeof window.requestIdleCallback !== 'undefined') {
// https://caniuse.com/#search=requestIdleCallback
setCallbackId(
window.requestIdleCallback(() => {
setRenderChild(true);
}),
);
} else {
setTimeout(() => {
setRenderChild(true);
});
}
}
}, []);
if (!isNode && !renderChild) {
return null;
}
// 取消这次渲染
if (!isNode && callbackId) {
window.cancelIdleCallback(callbackId);
}
return children;
};
同时页面埋点、性能检测相关的操作,也放到浏览器闲时的时候去触发。
📝备注:window.requestIdleCallback 需要加对应的 polyfill,推荐 requestidlecallback-polyfill 包去做 Safari 浏览器下的 polyfill。
卡片的 1.5 屏滚动渲染
由于飞天服务平台首页的业务特性,用户可以在首页配置非常多的卡片,如果同时去加载用户配置 10+ 个 卡片,肯定会触发性能问题。
性能表现上滚动到组件就立刻加载,肯定比 1.5 屏更好,但是 1.5 屏的渲染优化会让体验更加优秀,加载上有一个提前量,如果不是非常快速的滚动到对应卡片,体验上与直接默认加载卡片没有区别。
这里可以使用原生的 Intersection Observer API 去实现,浏览器本身提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。当然为了 API 更加友好,我使用了 react-intersection-observer这个组件库去做组件层面的懒加载。
充分利用缓存、用空间换时间
全局添加骨架屏,体感上更快
骨架屏,尤其是精度更高的骨架屏,可以在一定程度上让用户感觉更快了。所以在骨架屏这个事情上,主要做了两个方面的骨架屏
-
卡片的加载骨架屏,卡片开始渲染初期,预渲染卡片的内容骨架
-
表格的骨架屏(效果可以看文章开头的效果图)
SWR,利用缓存的数据优先渲染一次,代替高精度骨架屏
简单科普下 SWR。swr 是 stale-while-revalidate 的简称,一种由 HTTP RFC 5861 (opens in a new tab) 推广的 HTTP 缓存失效策略。最主要的能力是:我们在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染。
使用 SWR,组件将会不断地、自动获得最新数据流。UI 也会一直保持快速响应。SWR 特性在特定场景,对用户非常友好。
目前我们将用户的数据存储在浏览器端数据库(IndexDB)内,配合一定的数据更新、缓存、清理机制,在页面加载过程中优先使用之前缓存的数据做一次渲染,等请求的数据返回之后,再做一次对比重新渲染。如果数据没有变化,则不需要重新渲染。
这么做的优势也很明显:
-
优势是使用真实数据渲染
-
体感上没有数据加载过程,也就是看不到加载的 loading 效果
详细的技术方案,可以关注我,后续会出详细的技术文章来介绍。
使用 Service Woker 缓存资源文件
基于 Service Worker 优化的原理,这里不再赘述,有很多同类型的文章。
图示为缓存优先,回退到网络的 Service Worker 策略:
-
请求到达缓存。如果资源位于缓存中,请从缓存中提供。
-
如果请求不在缓存中,请转到网络。
-
网络请求完成后,将其添加到缓存中,然后从网络返回响应。
目前看 Service Worker 缓存作为浏览器端和服务端代理, 拦截请求,处理响应,的确会有很好的性能提升。目前飞天服务平台首页还没有使用这个方案,后续的继续优化和性能提升,是一个可以发力的地方。
总结和展望
可以先看一下优化前后的数据,目前优化后页面总体在 1s 左右,做到页面的秒开。
当然性能优化是没有尽头的,我这里主要是从上述三方面来讲述飞天服务平台实践过的性能优化。如果大家有更多想法也可以交流反馈。
总体而言,在做到了资源尽可能小,关键链路优先、非关键链路渐进式加载之后,Web 应用的性能已经不会很差了,当做好常规的性能优化之后,相信 Web 的体验已经是非常优秀的。这个时候如果需要追求极致的性能优化,结合 SWR 和 Service Worker 的缓存,充分将资源和数据请求缓存起来,可以将 Web 的体验提升到类似原生应用的体验档次,当然性能优化也要考虑性价比,对于极致追求页面打开速度的场景,可以多做这种方案的尝试。
后面这些性能优化的方案和策略,也同样会在飞天服务平台其他页面推广,相信伴随着这些策略的使用,飞天服务平台的页面初次打开体验可以提升一个档次。
相关链接
-
Servie Worker 的图来源:developer.chrome.com/docs/workbo...
-
requestidlecallback-polyfill 包链接:github.com/pladaria/re...