浏览器性能优化
正确的资源下载/执行优先级,并减少页面加载期间浏览器的空闲时间,是提升Web应用性能的最重要手段之一。在实际Web应用中,此优化方案被证明比减少代码大小更为直接有效,此类型的优化对产品开发节奏的影响比较小,它只需要少量的代码更改和重构。
Javascript
,XHR
,图片预加载
让浏览器在关键页面预加载动态资源:动态加载的JavaScript、预加载的XHR-GraphQL数据请求。
<link rel="preload" href="index.js" as="script" type="text/javascript">
动态加载JavaScript
,通常是指通过import('...')
为指定客户端由路由加载的脚本。在服务端接收到请求时,可以知道这个特定的服务端入口文件将需要哪些客户端路由的动态脚本,并且在页面初始化渲染的HTML中,为这些脚本添加预加载逻辑。
在某个页面入口文件中,必然会执行1个特定的GraphQL请求,可以预加载这个XHR请求,这个点非常重要,因为在某些场景下GraphQL请求会消耗大量时间,页面必须要等到这些数据加载好才能开始渲染。
<link rel="preload" href="/graphql/query?id=12345" as="fetch" type="application/json">
调整预加载优先级的好处
除了更早地开始资源加载,预加载还有额外的好处:提升异步脚本加载的网络优先级,对于重要的[异步脚本]来说,这点非常重要,因为它们的网络优先级默认是low
。这意味着它们的优先级和屏幕之外的图片一样(low
),而页面的XHR
请求和屏幕内的图片网络优先级则比它们要高(high
)。这导致页面渲染所需的重要脚本的加载可能被阻塞,或和其他请求共享带宽。
调整预加载优先级的问题
预加载的问题:它提供的额外控制会带来额外的责任,即设置正确的资源优先级。当在低速移动网络区域、慢WIFI网络或丢包率比较高的场景中测试时,<link rel="preload" as="script">
的网络请求优先级会比<script />
标签的JavaScript
脚本高,而<script />
标签的脚本才是页面渲染首先需要的,这将增加整个页面的加载时间。
只预加载路由需要的异步JavaScript
包
通过客户端路由当前页面需要异步加载的。
- 预加载所有
JavaScript
资源。 - 控制
JavaScript
资源加载的顺序。
图片预加载
构建一个优先任务的抽象来处理异步加载的队列,这个预加载任务在初始化时优先级是idle
(利用requestIdleCallback
函数,window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。),所以它会到浏览器不执行任何其他的重要任务时才开始。提升优先级的方法是通过取消所有待执行的空闲任务,这样预加载任务就能立即执行。
使用early flush
(提前刷新)和progressive HTML
(渐进式HTML
)来推送数据
如何让浏览器还没有任何服务端的HTML
返回就发起请求?解决方案是服务器主动向浏览器推送资源,这有点像利用http/2
的push
特性,它具有非常好的浏览器兼容性,并且不需要为实现此特性而增加服务端基础架构的复杂性。
它的主要实现包含两点:
HTTP
分块传输编码- 浏览器渐进式渲染
HTML
Chunked transfer encoding(分块传输编码)是HTTP/1.1
协议中的一部分,从本质上来看,它允许服务端将HTTP
的返回切碎成多个chunk
(块),然后以分流的形式传输给浏览器。浏览器不断接收这些块,在最后一个块到达后将它们聚合在一起。
它允许服务器在完成每个chunk
时,就将此时的HTML
页面的内容流式传输到浏览器,不必等待整个HTML
完成。服务器一收到请求,就可以将HTML
的头部flush
给浏览器(early flush),减少了处理HTML
剩余内容的时间。对于浏览器来说,浏览器在收到HTML
的头部时,就开始预加载静态资源和动态数据,此时服务器还在忙于剩余HTML
内容的生成。
利用[分块传输编码]在传输完成的同时讲其它数据推送到客户端。对于服务端渲染的Web应用,一般采用HTML
格式返回;对于SPA
,可以用JSON
格式数据推送到浏览器。
创建一个JSON
缓存来存储服务端返回的数据。
// 服务端将会写下所有它已经在准备的请求路径
// 这样客户端就知道等待服务端返回数据即可,不需要自己发送XHR请求
window.__data = {
'/my/api/path': {
// 客户端发起请求后的回调都存在waiting数组中
waiting: []
}
};
window.__dataLoaded = function (path, data) {
const cacheEntry = window.__data[path];
if (cacheEntry) {
cacheEntry.data = data;
for (let i = 0; i < cacheEntry.waiting.length; i++) {
cacheEntry.waiting[i].resolve(cacheEntry.data);
}
cacheEntry.waiting = [];
}
};
在把HTML
刷新到浏览器后,服务端就可以自己执行API
请求的查询,完成后将JSON
数据以[包含script
标签的HTML
片段]的形式刷新到页面中。当这个HTML
片段被浏览器接收并解析后,它会被数据写入JSON
缓存对象中。这里有个关键技术点:浏览器会在接收到chunks
的时候就立即开始渲染。所以,可以再服务端并行生成一系列API
数据,并在每个API
数据准备好时,就立即将其刷新到JS块中。
当客户端JS准备好请求某个特定数据的时候,它将先检查JSON缓存对象中有没有数据,而不是发起一个XHR请求。如果JSON缓存对象中已经有数据,它将立即得到返回;如果JSON缓存对象已标记pending
,它将把请求的resolve
回调注册到对应的waiting
数组中,请求完成后,执行对应的resolve
回调。
function queryAPI(path) {
const cacheEntry = window.__data[path];
if (!cacheEntry) {
// 没有缓存对象,直接发起一个普通的XHR请求
return fetch(path);
} else if (cacheEntry.data) {
// 服务端已经推送好数据
return Promise.resolve(cacheEntry.data);
} else {
// 服务端正在生成数据或推送中
// 把请求成功的resolve回调放到cacheEntry.waiting队列中
// 当接收到数据后,回调会按顺序执行
const waiting = {};
cacheEntry.waiting.push(waiting);
return new Promise((resolve) => {
waiting.resolve = resolve;
});
}
}
此项优化的效果非常明显:桌面端用户访问页面的渲染完成的时间减少14%,移动端用户(有更高的网络延迟)更是减少了23%。
缓存优先
如何让页面更快地获取到数据?唯一的思路就是不经由网络的请求和推送数据。可以采用缓存优先的渲染机制来实现
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。