5月28日 02:18

什么是 DNS 预解析?实现方式和踩坑要点有哪些

DNS 预解析(DNS Prefetching)是前端性能优化中低成本高收益的手段之一。浏览器在加载页面时提前解析可能用到的域名,把 DNS 查询结果缓存起来,等真正请求资源时跳过解析步骤,直接建立连接。

一次 DNS 解析通常耗时 20-120ms,在移动网络下可能更长。对于依赖多个跨域资源的页面,这些延迟会叠加。DNS 预解析把这些查询提前到页面加载的空闲时段,用户几乎感知不到。

DNS 解析的完整链路

理解预解析的前提是搞清楚 DNS 解析本身经历了什么:

  1. 浏览器缓存 — Chrome 对每条 DNS 记录缓存约 60s(TTL 由响应决定),命中则 0ms
  2. 操作系统缓存 — 命中系统缓存约 1-5ms
  3. 路由器缓存 — 家用路由器也有 DNS 缓存,约 15ms
  4. ISP DNS 缓存 — 运营商 DNS 服务器,常见域名 80-120ms,冷门域名 200-300ms
  5. 递归查询 — 从根域名服务器 → 顶级域名服务器 → 权威域名服务器逐级查询,耗时最长

预解析的作用范围是第 4-5 步:提前触发完整查询链路,把结果存入浏览器缓存,后续请求直接命中第 1 步。

实现方式

最常用、最推荐的方式:

html
<head> <meta charset="UTF-8"> <!-- 开启 DNS 预解析(HTTPS 页面默认关闭) --> <meta http-equiv="x-dns-prefetch-control" content="on"> <!-- 预解析 CDN 域名 --> <link rel="dns-prefetch" href="//cdn.example.com"> <!-- 预解析 API 域名 --> <link rel="dns-prefetch" href="//api.example.com"> </head>

关键细节

  • href 只需要写协议+域名,不需要路径
  • 标签放在 <head> 尽早位置,最好紧跟 <meta charset> 之后
  • HTTPS 页面默认不自动预解析超链接域名,需用 x-dns-prefetch-control 显式开启
  • 也可以用 content="off" 关闭自动预解析(减少隐私泄露风险)

2. preconnect:更进一步

html
<link rel="preconnect" href="//api.example.com" crossorigin> <link rel="dns-prefetch" href="//api.example.com">

preconnect 在 DNS 解析之外还完成了 TCP 握手和 TLS 协商,相当于把网络连接提前建好。crossorigin 属性指定 CORS 模式,如果目标资源需要跨域凭证则设为 use-credentials

dns-prefetch vs preconnect 选择策略

场景选择原因
当前页面确定会用到的资源preconnect建好完整连接,收益最大
可能会用的资源(如用户点击后加载)dns-prefetch只解析域名,资源消耗低
同时配置两者都写preconnect 不支持时回退到 dns-prefetch

浏览器对 preconnect 有数量限制(通常 6-8 个),超出部分会被忽略,所以只给关键域名用 preconnect。

在服务端响应头中配置,比 HTML 标签更早生效:

http
HTTP/1.1 200 OK Content-Type: text/html Link: <//cdn.example.com>; rel=dns-prefetch Link: <//api.example.com>; rel=preconnect

Nginx 配置:

nginx
location / { add_header Link '<//cdn.example.com>; rel=dns-prefetch'; add_header Link '<//api.example.com>; rel=preconnect'; }

这种方式在浏览器还没开始解析 HTML 时就生效,比 <link> 标签快一个 RTT。

4. JavaScript 触发

javascript
// 方式一:Image Hack(兼容性好) function prefetchDNS(hostname) { new Image().src = '//' + hostname + '/favicon.ico?' + Date.now(); } // 方式二:Fetch API(更规范) async function prefetchDNS(hostname) { try { await fetch('//' + hostname, { mode: 'no-cors' }); } catch (e) { // fetch 会因 CORS 失败,但 DNS 解析已经触发 } } // 方式三:动态创建 link 标签(最规范) function prefetchDNS(hostname) { const link = document.createElement('link'); link.rel = 'dns-prefetch'; link.href = '//' + hostname; document.head.appendChild(link); }

Image Hack 的原理:浏览器为加载图片必须先解析域名,即使图片最终 404 也无所谓,DNS 解析已经完成。这种方式兼容老浏览器,但不推荐在新项目中使用。

浏览器自动预解析

Chrome 和 Firefox 默认会扫描页面中的超链接和资源引用,自动预解析这些域名:

html
<!-- 浏览器会自动预解析 www.example.com --> <a href="https://www.example.com">链接</a> <!-- 浏览器会自动预解析 cdn.example.com --> <script src="https://cdn.example.com/script.js"></script>

手动添加 dns-prefetch 的意义在于:预解析页面中尚未出现但即将使用的域名,比如用户交互后才加载的 API 域名。

最佳实践

预解析哪些域名

按优先级排序:

  1. 首屏关键资源的域名 — CSS、关键 JS 所在的 CDN
  2. API 域名 — 页面必定请求的数据接口
  3. 第三方服务域名 — 统计、支付等确定会调用的服务
  4. 跳转目标域名 — 如果页面有明确的外链引导

避免过度预解析

html
<!-- 不要这样做:预解析几十个域名 --> <link rel="dns-prefetch" href="//a.example.com"> <link rel="dns-prefetch" href="//b.example.com"> <!-- ...20 个域名 --> <!-- 控制在 3-6 个关键域名 --> <link rel="dns-prefetch" href="//cdn.example.com"> <link rel="dns-prefetch" href="//api.example.com"> <link rel="dns-prefetch" href="//static.example.com">

每个预解析都会占用浏览器资源(DNS 查询、缓存条目),超过 10 个会适得其反。只预解析高概率会用到的域名。

与其他资源提示配合

html
<head> <!-- DNS 预解析:低优先级域名 --> <link rel="dns-prefetch" href="//analytics.example.com"> <!-- 预连接:关键域名 --> <link rel="preconnect" href="//api.example.com" crossorigin> <link rel="dns-prefetch" href="//api.example.com"> <!-- 预加载:确定要用的具体资源 --> <link rel="preload" href="/critical.css" as="style"> <link rel="preload" href="/app.js" as="script"> </head>

三者关系:dns-prefetch 解析域名 → preconnect 建立连接 → preload 下载具体资源。层层递进,按需使用。

需要注意的问题

隐私问题

DNS 预解析会向 DNS 服务器发送查询,即使用户最终没有访问该域名。这意味着:

  • ISP 可以通过 DNS 查询记录推断用户可能访问的站点
  • 在 HTTPS 页面上,Chrome 默认关闭对超链接的自动预解析,正是出于隐私考虑
  • 如果页面有敏感外链,可以用 x-dns-prefetch-control: off 关闭自动预解析

不适用于同域

html
<!-- 没有意义:同域 DNS 已在首次请求时解析 --> <link rel="dns-prefetch" href="//www.yoursite.com">

浏览器加载页面时已经解析了当前域名,对同域做预解析完全是浪费。

preconnect 的 crossorigin 陷阱

html
<!-- 错误:缺少 crossorigin,浏览器会建两个连接 --> <link rel="preconnect" href="//api.example.com"> <!-- 正确:根据资源类型设置 crossorigin --> <link rel="preconnect" href="//api.example.com" crossorigin> <!-- 如果资源需要凭证 --> <link rel="preconnect" href="//api.example.com" crossorigin="use-credentials">

crossorigin 不匹配会导致浏览器建立两条独立连接,反而浪费资源。规则:如果目标资源用 <script crossorigin>fetch 加载,preconnect 必须带 crossorigin

性能监控

用 Performance API 测量 DNS 解析耗时:

javascript
// 获取所有资源条目的 DNS 耗时 const entries = performance.getEntriesByType('resource'); entries.forEach(entry => { const dnsTime = entry.domainLookupEnd - entry.domainLookupStart; if (dnsTime > 0) { console.log(`${entry.name}: DNS 解析 ${dnsTime.toFixed(0)}ms`); } });

Chrome DevTools Network 面板中,每个请求的时间线里"Initial connection"之前的浅色段就是 DNS 查询。Lighthouse 审计中"Preconnect to required origins"和"Avoid DNS prefetch for the same domain"两条规则直接相关。

面试高频问题

Q: dns-prefetch 和 preconnect 有什么区别?什么时候用哪个?

dns-prefetch 只解析域名(20-120ms),preconnect 还完成 TCP 握手和 TLS 协商。当前页面确定要请求的域名用 preconnect,可能访问的用 dns-prefetch。浏览器对 preconnect 有数量限制,不要超过 6 个。

→ 追问:preconnect 的 crossorigin 属性有什么作用?不设置会怎样?

如果目标资源需要 CORS,preconnect 必须带 crossorigin 属性,否则浏览器会为有凭证和无凭证两种情况分别建连接,浪费资源。

Q: HTTPS 页面为什么默认不自动预解析超链接域名?

隐私考虑。DNS 查询是明文的,预解析会把用户可能访问的站点暴露给 ISP 和中间人。HTTPS 页面默认关闭自动预解析,但手动声明的 dns-prefetch 仍然生效。

→ 追问:那如何让 HTTPS 页面也自动预解析?

添加 <meta http-equiv="x-dns-prefetch-control" content="on">

Q: 什么时候不应该用 DNS 预解析?

三种情况:同域资源(已经解析过了,再预解析无意义);不确定是否使用的低频域名(浪费 DNS 查询和浏览器缓存);页面有隐私敏感外链时(关闭自动预解析)。

标签:DNS