你考虑过用link rel做性能优化吗?

202 阅读7分钟

我们来详细解析一下 HTML 中的 <link rel="..."> 元素。

<link> 标签是一个空元素(它没有闭合标签),它位于 HTML 文档的 <head> 部分,用于定义当前文档与外部资源之间的关系。rel 是 relationship 的缩写,这是整个标签的灵魂,它指明了链接资源与当前文档的关系。

基本语法

html

<head>
  <link rel="relationship-type" href="path-to-resource" [attributes]>
</head>
  • rel: (必需)定义关系类型。
  • href: (必需)指定外部资源的路径。
  • [attributes]: (可选)其他属性,如 astypemediacrossorigin 等,这些属性会根据 rel 的不同而发挥不同作用。

常见且重要的 rel 类型详解

1. 样式表 (Stylesheets)

这是最常见的使用场景,用于引入外部 CSS 文件。

  • rel="stylesheet"

    • 作用:定义指向外部样式表的链接。

    • 示例

      html

      <link rel="stylesheet" href="styles.css">
      
    • 注意:浏览器会以高优先级下载并解析它,会阻塞渲染直至样式表被加载和解析(但不会阻塞 HTML 本身的解析)。

2. 网站图标 (Favicon)

用于定义浏览器标签页、收藏夹中显示的小图标。

  • rel="icon"

    • 作用:指定页面图标。

    • 示例

      html

      <link rel="icon" href="favicon.ico" type="image/x-icon">
      <!-- 现代浏览器更推荐 PNG 格式 -->
      <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
      <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
      
  • rel="apple-touch-icon"

    • 作用:为 iOS 和 macOS Safari 设备指定主屏幕书签的图标。

    • 示例

      html

      <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
      

3. 预处理与预加载 (Performance & Preloading)

这是性能优化的关键,用于指示浏览器如何优先处理关键资源。

  • rel="preload" (极其重要)

    • 作用:以高优先级强制浏览器请求资源,且不阻塞文档解析。用于提前加载渲染初期所必需的关键资源(如关键CSS、Web字体、首屏图片)。

    • 必须与 as 属性一起使用,以告知浏览器资源类型,以便设置正确的优先级和请求头。

    • 示例

      html

      <!-- 预加载关键CSS -->
      <link rel="preload" href="critical.css" as="style" onload="this.rel='stylesheet'">
      <!-- 预加载Web字体,注意crossorigin -->
      <link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
      <!-- 预加载关键JS -->
      <link rel="preload" href="main.js" as="script">
      
  • rel="prefetch"

    • 作用:以低优先级在浏览器空闲时请求资源。用于预取未来可能用到的资源(如下一页面的资源)。

    • 示例

      html

      <link rel="prefetch" href="page-2.html">
      <link rel="prefetch" href="product-image.jpg">
      
  • rel="preconnect" / rel="dns-prefetch"

    • 作用:提前与第三方源建立连接(TCP握手、TLS协商)或仅进行DNS解析。用于减少发起第三方资源请求时的延迟。

    • 示例

      html

      <!-- 建立连接,开销更大但更彻底 -->
      <link rel="preconnect" href="https://cdn.example.com">
      <!-- 仅DNS解析,开销小 -->
      <link rel="dns-prefetch" href="https://cdn.example.com">
      
  • rel="modulepreload"

    • 作用:类似于 preload,但专门用于 ES6 模块。它会预加载并编译模块,使其可以立即执行。

    • 示例

      html

      <link rel="modulepreload" href="main.mjs">
      

4. 替代内容 (Alternatives)

  • rel="alternate" (用途多样)

    • 作用:指向当前文档的替代版本。

    • 常见用法

      • RSS/Atom Feed:

        html

        <link rel="alternate" type="application/rss+xml" href="rss.xml" title="RSS Feed">
        
      • 不同语言版本:

        html

        <link rel="alternate" hreflang="es" href="https://es.example.com/">
        
      • 不同媒体版本 (如打印版):

        html

        <link rel="alternate" media="print" href="printable-page.html">
        
  • rel="canonical" (SEO 重要)

    • 作用:指定页面的“权威”或“首选”版本,用于解决因不同URL可能显示相同内容而导致的重复内容问题。

    • 示例

      html

      <link rel="canonical" href="https://example.com/preferred-version/">
      

5. 其他有用类型

  • rel="manifest"

    • 作用:指向 Web App Manifest 文件(通常是 manifest.json),用于将网站安装为PWA(渐进式Web应用),并定义名称、图标、主题色等。

    • 示例

      html

      <link rel="manifest" href="manifest.json">
      

总结与实践建议

rel 类型主要用途优先级关键属性
stylesheet引入样式表hrefmedia
preload强制预加载关键资源最高hrefas (必须)
prefetch预取未来可能用的资源href
preconnect提前与第三方源建立连接href
icon / apple-touch-icon设置网站图标hrefsizestype
canonical解决重复内容,利于SEO-href
manifestPWA应用安装href

最佳实践建议:

  1. 性能优先:积极使用 preload 来加载关键字体、样式和脚本,这对提升 LCP(最大内容绘制)  指标非常有效。
  2. 使用 as:使用 preload 时务必加上 as 属性,否则浏览器可能无法正确分配优先级或发送正确的请求头。
  3. 按需使用prefetch 适用于用户下一步操作很可能用到的资源,不要滥用,以免浪费用户带宽。
  4. SEO 必备:为所有页面正确设置 canonical 链接,尤其是电商网站和有大量过滤参数的页面。
  5. PWA 基础:如果希望用户能“安装”你的网页应用,rel="manifest" 是必不可少的。

通过合理组合这些 <link rel> 类型,你可以极大地优化网站的加载性能、用户体验和搜索引擎友好度。

我们来思考一下,既然有了preload,为什么还有新增一个modulepreload呢?

简单来说,modulepreload 是 preload 专门为 ES 模块(ESM)设计的高效版本。虽然 preload 也能加载模块,但 modulepreload 做了两件关键事情:

  1. 递归地获取并缓存整个模块依赖图
  2. 提前编译和缓存模块,使其可以立即执行。

而 preload 只会获取单个文件,并且不知道也不处理该文件内部的依赖关系。

核心区别对比

让我们用一个例子来清晰地展示区别。假设我们有以下模块:

main.mjs:

javascript

import { helper } from './helper.mjs';
import { format } from 'https://cdn.example.com/utils.mjs';
console.log('Main module loaded');

helper.mjs:

javascript

console.log('Helper module loaded');

场景一:使用 rel="preload"

html

<link rel="preload" href="main.mjs" as="script">
  • 浏览器行为

    1. 识别到 preload,以高优先级去下载 main.mjs 文件。
    2. 下载完成後,将其放入缓存。
    3. 当解析到 <script type="module" src="main.mjs"> 时,浏览器从缓存中读取 main.mjs 文件内容,开始解析
    4. 解析过程中,发现 import './helper.mjs',于是发起一个新的、独立的新请求去获取 helper.mjs
    5. 发现 import 'https://cdn.example.com/utils.mjs',再发起另一个新请求去获取 utils.mjs
    6. 所有依赖都下载并解析完毕后,才执行模块。
  • 存在的问题

    • 瀑布式请求:虽然 main.mjs 本身被预加载了,但其内部的依赖仍然需要依次被发现、再请求。这产生了不必要的网络延迟,削弱了预加载的效果。
    • 没有编译优化preload 只是获取文件,并不会提前编译它。

    场景二:使用 rel="modulepreload"

html

<link rel="modulepreload" href="main.mjs">
<link rel="modulepreload" href="helper.mjs">
<link rel="modulepreload" href="https://cdn.example.com/utils.mjs">
  • 浏览器行为

    1. 识别到 modulepreload,以高优先级下载 main.mjs
    2. 浏览器会解析 main.mjs 的内容(注意这一步!),找出它所有的 import 和 export 语句。
    3. 发现它依赖 helper.mjs 和 utils.mjs,于是自动地、递归地以高优先级去下载这些依赖项。你甚至不需要手动为所有依赖都写 link 标签(但最佳实践是写上,以确保最高优先级)。
    4. 不仅下载这些模块,还会对其进行编译和缓存(构建模块作用域,处理导入导出接口等)。
    5. 当解析到 <script type="module" src="main.mjs"> 时,所有模块都已经在缓存中并且已经编译完成
    6. 浏览器可以立即执行它们,几乎没有任何延迟。

总结对比表格

特性rel="preload" (as="script")rel="modulepreload"
获取行为仅获取单个文件。递归获取整个依赖图(现代浏览器支持)。
处理内容只下载,不解析。下载并解析模块,发现其依赖。
执行准备只缓存文件。执行时仍需编译。提前编译模块,缓存编译结果
执行速度减少下载延迟,但执行前仍需编译。极大减少下载+编译延迟,可立即执行。
适用对象任何资源(脚本、样式、字体、图片等)。仅限 ES 模块

为什么需要新增 modulepreload

  1. 模块依赖图的特殊性:ES 模块的本质是一个可能很深的依赖树。优化单个文件的加载(preload)对于模块来说效果不彰,必须优化整个依赖图的加载才能带来质的提升。modulepreload 的设计初衷就是理解并优化这个依赖图。
  2. 编译成本显著:JavaScript 模块的解析和编译是重要的性能成本。preload 无法解决这个问题,而 modulepreload 通过提前编译消除了这个成本,使得模块一旦被请求执行就能得到结果。
  3. 为现代Web开发量身定制:现代前端框架(如 Vue、React、Svelte)几乎都基于 ES 模块构建,其产出的代码是由大量模块组成的复杂依赖图。modulepreload 是为优化这种现代应用架构而生的利器。

最佳实践

  • 对普通资源(如CSS、字体、非模块脚本、图片):使用 rel="preload"
  • 对ES模块:优先使用 rel="modulepreload"
  • 手动列出关键依赖:虽然浏览器会递归获取,但为了确保最高优先级,建议将关键链路上的所有核心模块都显式地用 <link rel="modulepreload"> 列出。构建工具(如 Vite、Webpack)未来很可能会自动注入这些标签。

总之,modulepreload 不是替代 preload,而是它在 ES 模块领域的深度扩展和专业化,专门为了解决模块加载中的特定性能瓶颈而生。