不要阻碍浏览器 preload scanner 的运行
我们都知道浏览器在渲染页面时候,其内部会帮开发者做各种优化,其中有一项就是preload scanner。通过这篇文章,我们可以了解什么是preload scanner,以及它是如何有助于性能的。
preload scanner 是什么?
我们都知道浏览器解析页面时候正常是通过html parser 从上往下解析标签。直到解析器遇到一个阻塞资源时候会暂停往下解析。比如遇到加载样式表带<link>标签、或者是没有async or defer的script标签。
在遇到外部css文件情况下,解析跟渲染都会被阻塞。因为阻塞了所以不会出现 flash of unstyled content (FOUC)的场景,简单理解就是如果不阻塞,继续往下解析渲染。那么就会导致从没有应用样式的状态突然跳变到应用样式的状态(因为你异步加载解析css去了)。
当 HTML Parser遇到没有async或者defer属性的script标签时候,同样也会阻塞页面的解析与渲染。
小知识:如果是
type = module属性的script标签,默认相当于加了defer属性。(defer:异步加载,但是等浏览器解析完才会执行)
浏览器之所以这么处理,是因为如果html parser继续工作,它不确定js脚本是否会去修改dom。这就是为什么我们一般建议在文档末尾去加载js脚本了。
我们上面说的都是浏览器在遇到上述场景时候,应该阻塞解析跟渲染的理由。但是光靠阻塞也不是最终解决问题的好办法,它会影响后面其他重要资源的加载,比如图片。于是我们的主角登场了,浏览器通过一种叫preloader scaner的东西来辅助html parser解决上述问题。
preloader scaner是推测性的,因为它是解析原始 html标签,以便在html parse之前提前发现需要加载的资源。
如果判断 preloader scanner 是否正常工作
我们通过一个人为的示例页面,preload-scanner-fights.glitch.me/artifically…
这里我们借助www.webpagetest.org/result/2301…
正如您在网络瀑布图看到的,即使文档解析和渲染被阻塞。preloader scanner还是发现 < img > 元素。如果没有这种优化,浏览器就不能在阻塞期间机会性地获取内容,
接下来我们看看在什么场景下preload scanner会失效?以及如果避免这种失效。
注入一个异步的脚本
假设在你的 < head > 标签中,其中包含一些内联的 JavaScript,如下所示:
<script>
const scriptEl = document.createElement('script');
scriptEl.src = '/yall.min.js';
document.head.appendChild(scriptEl);
</script>
js动态注入的脚本文件默认是async异步的。如果我们把这个内联的script放在link之后。那么我们会得到一个糟糕的结果。
其实也很好理解,我们之前说过preloader scanner它是通过解析html原始标签来进行推测,而我们是动态注入script标签,它肯定是推测不出来的。
在上述图你也可以看到动态js是在2.6s(css下载解析后)后才可以下载、解析执行。所以js脚本被阻塞了,这可能会影响页面的交互时间(TTI)。而上面的img标签是可以被preloader scanner发现的。
那如果我们改为外部引用js的方式呢?
<script src="/yall.min.js" async></script>
当然聪明的同学可能提出另外一个方法,通过rel = preload属性来让浏览器预先加载资源。这个方案当然有用,但是它会带来别的副作用。最主要的是我们不应该用hack的方式来为一个错误的方案兜底。
我们通过预加载方式hack的“修复”了这里的问题,但它引入了一个新问题: 前两个演示中的异步脚本(尽管在 < head > 中加载)以“低”优先级加载,而样式表以“最高”优先级加载。在预加载异步脚本的最后一个演示中,样式表仍然以“最高”优先级加载,但脚本的优先级已经提升到“最高”。导致 js脚本先于css被浏览器加载了。
当资源的优先级提高时,浏览器会为其分配更多的带宽。这意味着ーー即使样式表具有最高的优先级ーー脚本提高的优先级可能会导致带宽占用。 如果这个js资源刚好特别大就会显得我们页面打开链接特别慢。
所以最好就是script标签放在合适的位置,根据场景合理使用async与defer属性。
使用 JavaScript 进行懒加载
我们应该经常看到大家通过懒加载方式来加载图片吧。然而有时候可能会被我们误用。
<img data-src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
使data-前缀是 JavaScript 实现懒加载的一种常见模式。当图像滚动到可视区中时,延迟加载程序去掉 data-前缀,这意味着在前面的示例中,data-src 变成 src。提示浏览器获取资源。
preloader scanner读取data-src这样的前缀属性跟读取src或者srcset属性处理不同,所以preloader scanner确实发现不了图片资源因此不会提前加载。所以正常情况下懒加载的使用没有问题。
但是,假设如果一个图片按照上述的标签方式使用,而这个图像恰好又是在可视区中开始滚动。
如果一个图片资源恰好是LCP的一个元素时候,不恰当的懒加载操作,可能是在页面的样式表阻止渲染时,LCP就会受到影响。
对于懒加载图片,启动时候就处于viewport内的图片, 正确的解决方案如下:
<img src="/sand-wasp.jpg" alt="Sand Wasp" width="384" height="255">
在这个简化的示例中,结果是在慢速连接上对 LCP 进行了100毫秒的改进。这可能看起来不像是一个巨大的改进,但是当你认为解决方案只是一个html属性的优化,并且大多数网页比这组例子更复杂的时候。这样的优化变得越来越重要。
图像并不是唯一可能遭受次优延迟加载模式影响的资源类型。< iframe > 元素也可能受到影响,而且由于 < iframe > 元素可以加载许多子资源,因此性能的影响可能会严重恶化。
CSS 背景图片
前面我们提到过preloader scanner只会扫描row html makeup,它不会扫描你的css文件,因此我们会想到如果在css文件内,把图片作为背景属性会怎么样。
像 HTML 一样,浏览器将 CSS 处理成自己的对象模型,称为 CSSOM。如果在构造 CSSOM 时发现了外部资源,那么这些资源将在发现时被请求,而不是preloader scanner请求。
假设页面的 LCP 元素是一个具有 CSS 背景图像属性的元素。下面是资源加载时发生的情况:
在上面这种情况,preloader scanner就发挥不了作用了。那我们可能会想到另外一个解法:
<!-- Make sure this is in the <head> below any
stylesheets, so as not to block them from loading -->
<link rel="preload" as="image" href="lcp-image.jpg">
如果您的 LCP 对象来自背景图像 CSS 属性,但是该图像根据视口大小而变化,则需要在 < link > 元素上指定 imagesrcset 属性。
尽管Rel = preload的作用有限,但是它仍然可以帮助浏览器比其他方式更快地发现图像:
使用 rel = preload 降低 LCP 时间。虽然这个提示有助于解决这个问题,但是更好的选择可能是评估图像作为 LCP 元素是否必须从 CSS 加载。使用 < img > 标记,您可以更好地控制加载适合视口的图像,同时允许preloader scanner发现它。
内嵌了太多资源
我们都知道内联引用也是常用的一种引用资源的方式,而且它比可能外部引用更快,因为它不需要发出网络请求。它就在文档中,并且可以立即加载。然而,它也有明显的缺点:
-
如果不缓存 HTML ーー如果 HTML 响应是动态的,那么就不能缓存内联资源ーー那么内联资源永远不会被缓存。这会影响性能,因为内联资源不可重用。
-
即使可以缓存 HTML,内联资源也不会在文档之间共享。与可以跨整个源缓存和重用的外部文件相比,这降低了缓存效率。
-
如果内联资源过多,将延迟
preload scanner扫描文档时候发现后面的资源,因为下载额外的内联内容需要更长的时间。
以这个示例页面为例子preload-scanner-fights.glitch.me/inline-noth… 元素是页面顶部的图像,CSS 位于由 < link > 元素加载的单独文件中。该页面还使用了四种网页字体,这些字体在CSS 资源的文件中单独请求的。
现在,如果 CSS 和所有字体都内联为 base64资源,会发生什么情况?
在这个例子中,内联的影响会对 LCP 产生负面影响,并且一般来说会对性能产生负面影响。不内联任何内容的页面版本只需3.5秒就可以绘制 LCP 图像。内联所有内容的页直到刚刚超过7秒时才绘制 LCP 图像。
这不仅仅是preloader scanner的问题。内联字体不是一个很好的策略,因为 base64是一种低效的二进制资源格式。另一个影响因素是,除非 CSSOM 认为有必要,否则不会下载外部字体资源。当这些字体内联为 base64时,无论当前页面是否需要它们,它们都会被下载。
所以对于内联到 HTML 中的内容要非常小心,特别是 base64编码的资源。一般来说,除了非常小的资源外,不推荐使用它。内嵌越少越好,因为内嵌太多是在玩火。
CSR 渲染
毫无疑问: JavaScript 肯定会影响页面速度。开发人员不仅依赖它来提供交互性,而且还倾向于依赖它来交付内容本身。这在某些方面会带来更好的开发人员体验; 但是对开发人员的好处并不总是转化为对用户的好处。
一种可以打败preloader scanner的模式是 CSR:
你可以看示例页面preload-scanner-fights.glitch.me/client-rend…
html是通过外部js进行渲染的。
render(content, document.body);
本质就是我们现在流行的CSR。
所以举个例子如果你的LCP元素是图片比较多的大,那么CSR就会对你的LCP造成影响。所以有时候可以考虑服务器端渲染(SSR)或静态生成的标记,因为它将帮助preloader scanner提前发现并机会性地获取重要资源。