写点什么

谷歌性能主管最新的有关 LCP 的文章

作者:Yestodorrow
  • 2023-03-24
    北京
  • 本文字数:5025 字

    阅读完需:约 16 分钟

原文地址:philipwalton.com/articles/dy…

动态 LCP 优先级:根据历史调整

2022 年 12 月 28 日

年初,chrome 新增Priority Hints API,允许开发者在 img、script 和 link 等元素上设置fetchpriority="high"以保证优先加载。我个人非常认可这个 api。我认为如果网站使用这个 api,能最快速、最便捷的提高LCP

不过也有小弊端,为了使用该 api,就必须提前知道页面的 LCP 是哪个元素。

对于绝大多数内容都是静态内容的网站,LCP 元素很容易知道。但是对于复杂网站,特别是有很多动态的、个性化甚至是 UGC 内容,可能就很难提前知道 LCP 元素是哪一个。

知道 LCP 元素最可靠的办法便是真实加载页面,然后查看通过 LCP 的 api 查看。但在这时,元素已经渲染到屏幕上了,再添加fetchpriority已经为时已晚。

真的如此吗?

尽管对于当前访问者,添加属性可能已经晚了,但是对于下一个访问者,是不一样的。难的是从上一个用户收集 LCP 数据,然后利用数据为未来做加载优化。尽管这听起来很复杂,但通过以下几个简单步骤,还是可以实现的:

  1. 在每个页面执行 js 脚本,检测 LCP 元素,发送到服务器

  2. 服务器存储 LCP 元素数据,便于将来参考

  3. 针对页面访问,检查页面是否有 LCP 元素,如有则对该元素添加fetchpriority属性。

尽管实现方法很多,我目前知道最好的是使用Cloudflare Workers, the Worker KV datastore和 Worker HTMLRewriter API,目前都有免费的方法。

本文我将详细介绍。此外为了方便,我也把代码简化做了例子,但如果想看完整方案,可以参照github链接

Step 1: 识别 LCP 元素发送到服务器

为了根据访问数据动态设置 LCP 元素的优先级别,第一步应该是判断 LCP 元素,标识该元素后在随后的用户访问中匹配该元素。

使用web-vitals 识别 LCP 元素很容易,而且第 3 版本还包含一个 属性,能够暴漏所有 LCP 信息,下面便是一个例子。

// Import from the attribution build.import {onLCP} from 'web-vitals/attribution';
// Then register a callback to run after LCP.onLCP(({attribution}) => { // If the LCP element is an image, send a request to the `/lcp-data` // endpoint containing the page's URL path and LCP element selector. if (attribution.lcpEntry?.element?.tagName.toLowerCase() === 'img') { navigator.sendBeacon( '/lcp-data', JSON.stringify({ url: location.pathname, selector: attribution.element, }) ); }});复制代码
复制代码

上面的代码例子中,attribution.element值是 css 选择器能够标识 LCP 元素。比如,在下面My Challenge to the Web Performance Community的页面中,通常将发送以下信息:

{  "url": "/articles/my-challenge-to-the-web-performance-community/",  "selector": "#post>div.entry-content>figure>a>img"}复制代码
复制代码

注意,通常会被发送,不是 100%发送。因为根据屏幕尺寸,当前页面最大元素不一定总是该图片。比如,下图就展示了在传统 PC 和移动端不同视口下最常见的 LCP 元素。



如上图所示,PC 端最大可见元素通常是图片,但在移动端通常就是第一段文本。

讲到这里需要强调一点,正如代码例子展示的,虽然同一个页面在不同用户可能 LCP 元素也可能是不同的,所以任何动态 LCP 方案都需要考虑这一点,我也会在文章后面讲解我是如何处理这个问题的。

Step 2: 存储 LCP 数据以便将来参考

第一步中的代码将当前页面的 LCP 数据发送到 /lcp-data端口,下一步是创建数据接收的处理方法并存储数据,以便将来访问使用。

因为我在网站上使用 cloudflare worker,对我来说最好的方式是使用KV存储。KV 存储是 key/value 数据结构,可以同步到 cloudflare 的边缘节点上,也正是因为如此,所以读取也非常快,不过在更新数据时,不能立刻同步到所有边缘节点上,所以数据可能不是最新的。

在我这个案例中,数据不是最新完全不是问题,因为该功能主要在于提升性能,任何的请求延迟都可能达不到初衷,同时,因为 Priority hints 严格来讲是锦上添花,即便 LCP 并非实时也不影响。

为了使用 cloudfalre kv 存储的边缘节点,首先需要创建存储然后设置binding,设置完成后,便能读写存储做简单的.get() 和 .put()操作了:

// Read from the store.const myValue = await store.get('my-key');
// Write to the store.await store.put('my-key', 'Updated value...');复制代码
复制代码

在我的 LCP 存储上,我希望能根据用户请求的页面来查询 LCP 元素选择器,所以 key 是页面 url,value 是该 url 的 LCP 元素选择器。

需要记住的是,因为 LCP 元素在不同的屏幕设备上可能也不相同,就需要根据在 lcp 数据上加上设备类型。为了判断设备类型(mobile 或 pc),我根据 ​​sec-ch-ua-mobile在请求时的 header 来判断 。尽管这一请求头只在 chromium 为基础的浏览器才兼容,要注意本身 priority hints 这个 api 也是如此,所以这个案例中已经足够用了。

参照上面内容,pc 端的 kv 键值对如下:

移动端的数据大概如下:

以下是完整的存储 LCP 数据的 worker 代码:

export default {  async fetch(request, env) {    if (url.endsWith('/lcp-data') && request.method === 'POST') {      return storePriorityHints(request, env.PRIORITY_HINTS);    }  },};
async function storePriorityHints(request, store) { const {url, selector} = await request.json();
// Determine if the visitor is on mobile or desktop via UA client hints. const device = request.headers.get('sec-ch-ua-mobile') === '?1' ? 'mobile' : 'desktop';
// The key is the device joined with the URL path. If the LCP element // can vary by more than just the device, more granularity can be added. const key = `${device}:${url}`;
// If the new selector is different from the old selector, update it. const storedSelector = await store.get(key); if (selector !== storedSelector) { await store.put(key, selector); }
// Return a 200 once successful. return new Response();}复制代码
复制代码

下面解释一下上述代码为什么这么写:

  1. export fetch 函数处理逻辑,包括检查请求 url 的请求方式(post)同时满足接口路径(/lcp-data)

  2. 如果都满足,调用 storePriorityHint 方法根据 PRIORITY_HINTS 的 kv 数据存储。

  3. storePriorityHint 会提取 url 和选择器的值,同时根据请求头是 sec-ch-ua-mobile 确定设备类型

  4. 随后检查 KV 存储利用 key 查找 LCP 的元素选择器并用 key 将设备和 url 关联。

  5. 如果能找到选择器,或者已存储的选择器与当前 json 内容不同,就会更新选择器。

Step 3: 添加匹配 fetchpriority 数据到将来的请求上

每个页面和设备的 LCP 数据存储后,就能够在给未来访问中动态的给 img 元素添加fetchpriority属性。

我之前提到使用HTMLRewriter能实现,因为能便捷使用 selector-based API 来重写 HTML,轻松找到之前存储的正好符合选择器的 img 元素。

逻辑如下:

  1. 对于每个页面的请求,根据页面 url、sec-ch-ua-mobile请求头来判定当前页面 LCP 数据的 key,然后再 KV 存储中查找该数据。

  2. 同时,请求当前页面的 HTML

  3. 如果当前页面/设备存储了 LCP 数据,则创建 HTMLRewriter实例,并找到匹配的元素选择器添加fetchpriority 属性。

  4. 如果未存储 LCP 数据,则按照正常返回页面

以下是代码:

export default {  async fetch(request, env) {    // If the request is to the `/lcp-data` endpoint, add it to the KV store.    if (url.endsWith('/lcp-data') && request.method === 'POST') {      return storePriorityHints(request, env.PRIORITY_HINTS);    }    // For all other requests use the stored LCP data to add the    // `fetchpriority` attribute to matching <img> elements on the page.    return addPriorityHintsToResponse(request, env.PRIORITY_HINTS);  },};
async function addPriorityHintsToResponse(request, store) { const urlPath = new URL(request.url).pathname; const device = request.headers.get('sec-ch-ua-mobile') === '?1' ? 'mobile' : 'desktop';
const hintKey = `${device}:${encodeURIComponent(urlPath)}`;
const [response, hintSelector] = await Promise.all([ fetch(request), store.get(hintKey), ]);
// If a stored selector is found for this page/device, apply it. if (hintSelector) { return new HTMLRewriter() .on(hintSelector, new PriorityHintsHandler()) .transform(response); } return response;}
class PriorityHintsHandler { #applied = false; element(element) { // Only apply the `fetchpriority` attribute to the first matching element. if (!this.#applied) { element.setAttribute('fetchpriority', 'high'); this.#applied = true; } }}复制代码
复制代码

可以通过在 pc 端访问网站页面 查看例子,应该能看到第一章图片使用了fetchpriority="high"属性,需要注意的是源代码中没有该属性,这个属性只有在之前用户上报该图片是 LCP 元素才会生效,你在访问时可能就能看到。

必须着重说明一下,该属性是被添加到了 HTML 上,而不是使用客户端 js 加载的。可以通过 curl 请求页面 html 源代码,在相应的 html 文件中应该能看到fetchpriority属性。

curl https://philipwalton.com/articles/my-challenge-to-the-web-performance-community/复制代码
复制代码

同时,源代码中没有这个属性,是 cloudflare 根据之前的访问情况添加的。

重要提醒

我认为大型网站在这上面能获益良多,但是有一些重要提醒需要关注一下。

首先,在上面的策略中,页面 LCP 元素需要在 HTML 源码中能找到,换言之,LCP 元素不能是 js 动态引入的,也不能是通过data-src而非src等常见技术:

<img data-src="image.jpg" class="lazyload" />复制代码
复制代码

In addition to the fact that it’s always a bad idea to lazy load your LCP element, any time you use JavaScript to load images, it won’t work with the declarative fetchpriority attribute.

总是懒加载 LCP 元素可能是很糟糕的方法,所以任何时候使用 js 加载图片,使用fetchpriority这个属性其实无效。

此外,如果访问者不同,lcp 元素变动不大,则使用这种方法收益最大。如果访问者不同,LCP 元素不同,则一个访问者的 LCP 元素则不会匹配下一个访问者。

如果网站是这种情况,你可能就需要在 LCP 数据 key 中添加 user ID;但是这种情况只在用户频繁反复访问网站的情况才值得。如果不是这种情况,可能不会有收益。

验证技术是否有效

同任何性能方案一样,观测影响和验证技术是否有效也非常重要。其中一个验证的方式是测量特定网站结果的精确度,也就是说,对于动态加载 fetchpriority的情况,有多少更正了元素。

可以使用下面代码验证:

import {onLCP} from 'web-vitals/attribution';
onLCP((metric) => { let dynamicPriority = null;
const {lcpEntry} = metric.attribution;
// If the LCP element is an image, check to see if a different element // on the page was given the `fetchpriority` attribute. if (lcpEntry?.url && lcpEntry.element?.tagName.toLowerCase() === 'img') { const elementWithPriority = document.querySelector('[fetchpriority]'); if (elementWithPriority) { dynamicPriority = elementWithPriority === lcpEntry.element ? 'hit' : 'miss'; } } // Log whether the dynamic priority logic was a hit, miss, or not set. console.log('Dynamic priority:', dynamicPriority);});复制代码
复制代码

如果曾动态加载fetchpriority,一旦页面出现fetchpriority属性,但非 LCP 元素,则添加错误。

你可以使用 “hit rate”验证匹配逻辑的有效情况,如果在一段时间内缺失很大,则有可能需要调整或取消。

总结

如果读者感觉这一技术有用,可以考虑尝试或者分享给你认为可能有用的人。我希望像 cloudflare 一样的 CDN 商能够自动使用这个技术,或者作为一项用户选可配置的特性。

此外,希望本文能对用户在 LCP 这方面有所启发,能了解 LCP 这种动态指标的实质是非常依靠用户的行为。虽然永远提前知道 LCP 元素不太可能,但是性能技巧在一定程度上能够有所收益,也需要适当的调整。


著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


发布于: 刚刚阅读数: 7
用户头像

Yestodorrow

关注

还未添加个人签名 2017-10-19 加入

还未添加个人简介

评论

发布
暂无评论
谷歌性能主管最新的有关LCP的文章_性能_Yestodorrow_InfoQ写作社区