什么是 DNS 预解析?实现方式和踩坑要点有哪些
DNS 预解析(DNS Prefetching)是前端性能优化中低成本高收益的手段之一。浏览器在加载页面时提前解析可能用到的域名,把 DNS 查询结果缓存起来,等真正请求资源时跳过解析步骤,直接建立连接。
一次 DNS 解析通常耗时 20-120ms,在移动网络下可能更长。对于依赖多个跨域资源的页面,这些延迟会叠加。DNS 预解析把这些查询提前到页面加载的空闲时段,用户几乎感知不到。
DNS 解析的完整链路
理解预解析的前提是搞清楚 DNS 解析本身经历了什么:
- 浏览器缓存 — Chrome 对每条 DNS 记录缓存约 60s(TTL 由响应决定),命中则 0ms
- 操作系统缓存 — 命中系统缓存约 1-5ms
- 路由器缓存 — 家用路由器也有 DNS 缓存,约 15ms
- ISP DNS 缓存 — 运营商 DNS 服务器,常见域名 80-120ms,冷门域名 200-300ms
- 递归查询 — 从根域名服务器 → 顶级域名服务器 → 权威域名服务器逐级查询,耗时最长
预解析的作用范围是第 4-5 步:提前触发完整查询链路,把结果存入浏览器缓存,后续请求直接命中第 1 步。
实现方式
1. HTML link 标签
最常用、最推荐的方式:
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。
3. HTTP Link 头部
在服务端响应头中配置,比 HTML 标签更早生效:
httpHTTP/1.1 200 OK Content-Type: text/html Link: <//cdn.example.com>; rel=dns-prefetch Link: <//api.example.com>; rel=preconnect
Nginx 配置:
nginxlocation / { 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 域名。
最佳实践
预解析哪些域名
按优先级排序:
- 首屏关键资源的域名 — CSS、关键 JS 所在的 CDN
- API 域名 — 页面必定请求的数据接口
- 第三方服务域名 — 统计、支付等确定会调用的服务
- 跳转目标域名 — 如果页面有明确的外链引导
避免过度预解析
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 查询和浏览器缓存);页面有隐私敏感外链时(关闭自动预解析)。