原文地址: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 WorkerKV datastore和WorkerHTMLRewriter 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元素。

谷歌性能主管最新的有关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 键值对如下:

Key desktop:/articles/my-challenge-to-the-web-performance-community/
Value #post>div.entry-content>figure>a>img

移动端的数据大概如下:

Key mobile:/articles/my-challenge-to-the-web-performance-community/
Value #post>div.entry-content>p

以下是完整的存储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 declarativefetchpriorityattribute.

总是懒加载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元素不太或许,可是功能技巧在一定程度上能够有所收益,也需求恰当的调整。

本文正在参加「金石计划」