跳到主要内容

从输入 URL 到页面展示

问题

从输入 URL 到页面展示,这中间发生了什么?

答案

这是一个超高频面试题,涵盖了网络、浏览器、渲染等多个知识点。整个过程可以分为以下几个阶段:

完整流程概览

阶段一:URL 解析与处理

1.1 用户输入处理

当用户在地址栏输入内容时,浏览器进程的 UI 线程会判断输入内容:

function handleUserInput(input: string): void {
if (isValidURL(input)) {
// 是有效 URL,准备导航
navigateTo(input);
} else {
// 不是有效 URL,使用搜索引擎
const searchURL = `https://www.google.com/search?q=${encodeURIComponent(input)}`;
navigateTo(searchURL);
}
}

function isValidURL(input: string): boolean {
// 检查是否符合 URL 格式
// 包含协议(http://、https://)
// 或者是有效域名(example.com)
const urlPattern = /^(https?:\/\/)?[\w\-]+(\.[\w\-]+)+/i;
return urlPattern.test(input);
}

1.2 URL 结构解析

https://www.example.com:443/path/page.html?name=test&id=1#section
│ │ │ │ │ │
│ │ │ │ │ └── Fragment(片段标识符)
│ │ │ │ └── Query String(查询参数)
│ │ │ └── Path(路径)
│ │ └── Port(端口)
│ └── Host(主机名)
└── Protocol(协议)
interface URLComponents {
protocol: string; // 'https:'
host: string; // 'www.example.com:443'
hostname: string; // 'www.example.com'
port: string; // '443'
pathname: string; // '/path/page.html'
search: string; // '?name=test&id=1'
hash: string; // '#section'
}

// 使用 URL API 解析
const url = new URL('https://www.example.com:443/path/page.html?name=test#section');
console.log(url.hostname); // 'www.example.com'
console.log(url.pathname); // '/path/page.html'

1.3 检查 HSTS(HTTP Strict Transport Security)

浏览器会检查该域名是否在 HSTS 预加载列表中,如果是,则强制使用 HTTPS:

// HSTS 检查逻辑
function checkHSTS(hostname: string): boolean {
// 1. 检查 HSTS 预加载列表
if (HSTS_PRELOAD_LIST.includes(hostname)) {
return true;
}

// 2. 检查之前响应头中的 Strict-Transport-Security
const cachedHSTS = hstsCache.get(hostname);
if (cachedHSTS && cachedHSTS.maxAge > Date.now()) {
return true;
}

return false;
}

阶段二:DNS 解析

2.1 DNS 解析流程

DNS(Domain Name System) 将域名解析为 IP 地址。

2.2 DNS 缓存层级

层级位置缓存时间
浏览器缓存Chrome 等浏览器内部约 1 分钟
操作系统缓存系统 DNS 客户端由 TTL 决定
hosts 文件本地配置文件永久(手动更新)
本地 DNS 服务器路由器/ISP DNS由 TTL 决定
权威 DNS 服务器域名注册商配置源数据
# 查看 DNS 解析过程
nslookup www.example.com

# macOS 刷新 DNS 缓存
sudo dscacheutil -flushcache

# 查看 hosts 文件
cat /etc/hosts

2.3 DNS 记录类型

类型说明示例
A域名到 IPv4 地址example.com -> 93.184.216.34
AAAA域名到 IPv6 地址example.com -> 2606:2800:...
CNAME域名别名www.example.com -> example.com
MX邮件服务器example.com -> mail.example.com
NS域名服务器example.com -> ns1.example.com
TXT文本记录用于 SPF、DKIM 等验证

2.4 DNS 优化

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//cdn.example.com">

<!-- 预连接(包含 DNS + TCP + TLS) -->
<link rel="preconnect" href="https://api.example.com">

阶段三:建立 TCP 连接

3.1 三次握手

TCP 是面向连接的协议,需要通过三次握手建立连接:

步骤客户端服务器说明
第一次握手发送 SYN接收客户端请求建立连接
第二次握手接收发送 SYN+ACK服务器确认并请求连接
第三次握手发送 ACK接收客户端确认,连接建立
为什么是三次握手?
  • 两次不够:服务器无法确认客户端收到了响应
  • 四次没必要:第二次握手可以合并 SYN 和 ACK
  • 核心目的:确认双方的发送和接收能力都正常

3.2 四次挥手(连接关闭)

为什么四次挥手?

TCP 是全双工通信,双方都可以发送数据。关闭连接时需要分别关闭两个方向的通道,因此需要四次挥手。

阶段四:TLS 握手(HTTPS)

如果是 HTTPS 请求,TCP 连接建立后还需要进行 TLS 握手:

4.1 TLS 1.2 握手流程

4.2 TLS 1.3 优化(1-RTT)

TLS 1.3 将握手从 2-RTT 减少到 1-RTT

4.3 连接复用优化

技术说明效果
HTTP Keep-AliveTCP 连接复用避免重复三次握手
TLS Session ResumptionTLS 会话复用避免重复 TLS 握手
HTTP/2多路复用单连接并行请求
HTTP/3 (QUIC)基于 UDP0-RTT 连接建立

阶段五:发送 HTTP 请求

5.1 构建 HTTP 请求

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session_id=abc123
Cache-Control: no-cache

5.2 请求方法

方法说明幂等性请求体
GET获取资源
POST创建资源
PUT更新资源(完整替换)
PATCH更新资源(部分更新)
DELETE删除资源
HEAD获取响应头
OPTIONS预检请求

阶段六:服务器处理请求

6.1 服务器处理流程

6.2 HTTP 响应

HTTP/1.1 200 OK
Date: Thu, 13 Feb 2026 10:00:00 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Content-Encoding: gzip
Cache-Control: max-age=3600
ETag: "abc123"
Connection: keep-alive

<!DOCTYPE html>
<html>
...
</html>

6.3 常见状态码

状态码含义说明
200OK请求成功
301Moved Permanently永久重定向
302Found临时重定向
304Not Modified资源未修改(协商缓存命中)
400Bad Request请求语法错误
401Unauthorized需要身份验认证
403Forbidden拒绝访问
404Not Found资源不存在
500Internal Server Error服务器内部错误
502Bad Gateway网关错误
503Service Unavailable服务不可用

阶段七:浏览器解析渲染

7.1 渲染流程概览

7.2 HTML 解析 → DOM 树

// HTML 解析过程
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
</head>
<body>
<div id="app">
<h1>Hello</h1>
<p>World</p>
</div>
</body>
</html>
`;

// 解析为 DOM 树
// Document
// └── html
// ├── head
// │ └── title
// │ └── "Example"
// └── body
// └── div#app
// ├── h1
// │ └── "Hello"
// └── p
// └── "World"

7.3 CSS 解析 → CSSOM 树

// CSS 解析过程
const css = `
body { font-size: 16px; }
#app { margin: 20px; }
h1 { color: red; }
p { color: blue; }
`;

// 解析为 CSSOM 树,并计算样式
// 每个节点包含 computed style

7.4 构建 Render 树

Render 树特点
  • 不包含不可见元素display: none 的元素不会出现在 Render 树中
  • 包含伪元素::before::after 会出现在 Render 树中
  • head 标签不渲染<head> 及其子元素不会出现在 Render 树中

7.5 Layout(布局/重排)

计算每个元素的精确位置和大小

interface LayoutBox {
x: number; // 相对于视口的 x 坐标
y: number; // 相对于视口的 y 坐标
width: number; // 元素宽度
height: number; // 元素高度
}

// 获取元素的布局信息
const element = document.getElementById('app');
const rect = element.getBoundingClientRect();
console.log(rect); // { x: 20, y: 20, width: 500, height: 300, ... }

7.6 Paint(绑制)

将布局信息转换为绑制指令

// 绑制指令示例
const paintCommands = [
{ type: 'drawRect', x: 0, y: 0, width: 1200, height: 800, color: 'white' },
{ type: 'drawRect', x: 20, y: 20, width: 500, height: 300, color: 'transparent' },
{ type: 'drawText', x: 20, y: 50, text: 'Hello', color: 'red', font: '32px Arial' },
{ type: 'drawText', x: 20, y: 100, text: 'World', color: 'blue', font: '16px Arial' },
];

7.7 Composite(合成)

将不同图层合成为最终画面:

会创建新图层的情况

触发条件说明
will-change显式提示浏览器
transform: translateZ(0)3D 变换
position: fixed固定定位
<video><canvas>特殊元素
opacity 动画透明度动画
filterCSS 滤镜

7.8 JavaScript 的影响

优化策略

<!-- 1. 脚本放在 body 底部 -->
<body>
<div id="app"></div>
<script src="app.js"></script>
</body>

<!-- 2. 使用 defer:HTML 解析完成后执行 -->
<script defer src="app.js"></script>

<!-- 3. 使用 async:下载完成后立即执行 -->
<script async src="analytics.js"></script>
属性下载执行时机执行顺序
阻塞解析下载后立即执行按顺序
defer并行下载HTML 解析完成后按顺序
async并行下载下载完成后立即执行不保证顺序

阶段八:页面交互

8.1 事件循环

页面渲染完成后,浏览器进入事件循环,等待用户交互:

// 事件循环简化模型
while (true) {
// 1. 执行宏任务
const macroTask = macroTaskQueue.shift();
if (macroTask) {
execute(macroTask);
}

// 2. 执行所有微任务
while (microTaskQueue.length > 0) {
const microTask = microTaskQueue.shift();
execute(microTask);
}

// 3. 检查是否需要渲染
if (shouldRender()) {
// 执行 requestAnimationFrame 回调
executeRAFCallbacks();
// 渲染页面
render();
}

// 4. 执行 requestIdleCallback(空闲时)
if (isIdle()) {
executeIdleCallbacks();
}
}

8.2 用户交互流程

完整流程总结

各阶段耗时参考

阶段典型耗时优化方向
DNS 解析20-120msDNS 预解析、CDN
TCP 连接20-100msKeep-Alive、HTTP/2
TLS 握手50-200msTLS 1.3、Session 复用
HTTP 请求50-500msCDN、Gzip、缓存
HTML 解析10-100ms减少 DOM 节点
CSS 解析10-50ms减少选择器复杂度
JS 执行50-500ms代码分割、延迟加载
Layout10-100ms减少重排
Paint10-50ms减少绘制区域
Composite5-20ms合理分层

常见面试问题

Q1: 从输入 URL 到页面展示的完整过程是什么?

答案

完整流程(按时间顺序):

  1. URL 解析:浏览器解析 URL,判断是网址还是搜索词
  2. 检查缓存:检查强缓存是否命中
  3. DNS 解析:将域名解析为 IP 地址
  4. 建立 TCP 连接:三次握手
  5. TLS 握手:如果是 HTTPS,建立加密通道
  6. 发送 HTTP 请求:构建请求报文并发送
  7. 服务器处理:服务器处理请求并返回响应
  8. 接收响应:浏览器接收响应数据
  9. 解析 HTML:构建 DOM 树
  10. 解析 CSS:构建 CSSOM 树
  11. 执行 JavaScript:可能修改 DOM/CSSOM
  12. 构建 Render 树:合并 DOM 和 CSSOM
  13. Layout:计算元素位置和大小
  14. Paint:生成绘制指令
  15. Composite:合成图层并显示
// 简化的时间线
interface NavigationTiming {
domainLookupStart: number; // DNS 开始
domainLookupEnd: number; // DNS 结束
connectStart: number; // TCP 开始
secureConnectionStart: number;// TLS 开始
connectEnd: number; // 连接建立完成
requestStart: number; // 请求开始
responseStart: number; // 首字节到达
responseEnd: number; // 响应结束
domInteractive: number; // DOM 可交互
domContentLoadedEventEnd: number; // DOMContentLoaded
loadEventEnd: number; // load 事件结束
}

Q2: 如何优化从 URL 到页面展示的性能?

答案

网络层优化

阶段优化策略
DNSDNS 预解析、使用 CDN
TCPKeep-Alive、HTTP/2 多路复用
TLSTLS 1.3、Session 复用
HTTPGzip 压缩、缓存策略、资源合并

渲染层优化

阶段优化策略
HTML减少 DOM 深度和节点数
CSS避免复杂选择器、减少重排
JavaScript代码分割、延迟加载、Web Worker
布局避免强制同步布局
绘制使用 transform/opacity 动画
// 关键渲染路径优化
// 1. 内联关键 CSS
<style>
/* 首屏关键样式 */
.header { ... }
.hero { ... }
</style>

// 2. 延迟非关键 CSS
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">

// 3. 延迟 JavaScript
<script defer src="app.js"></script>

// 4. 预加载关键资源
<link rel="preload" href="hero.jpg" as="image">

Q3: 为什么 CSS 要放在 head 中,JavaScript 要放在 body 底部?

答案

CSS 放在 head 的原因

  • CSS 不会阻塞 DOM 解析,但会阻塞渲染
  • 如果 CSS 放在底部,浏览器会先渲染无样式内容(FOUC:Flash of Unstyled Content)
  • 放在 head 中可以让浏览器尽早开始下载和解析 CSS

JavaScript 放在 body 底部的原因

  • 传统 <script> 会阻塞 DOM 解析
  • 放在底部可以让 DOM 先解析完成,页面更快呈现
  • JavaScript 执行时通常需要操作 DOM,DOM 需要先构建完成
<!DOCTYPE html>
<html>
<head>
<!-- CSS 放在 head -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="app">...</div>

<!-- JavaScript 放在 body 底部 -->
<script src="app.js"></script>
</body>
</html>

<!-- 现代方案:使用 defer -->
<head>
<link rel="stylesheet" href="styles.css">
<script defer src="app.js"></script>
</head>

Q4: TCP 三次握手和四次挥手的过程是什么?为什么挥手比握手多一次?

答案

三次握手

  1. 客户端 → 服务器:SYN(请求连接)
  2. 服务器 → 客户端:SYN+ACK(确认+请求连接)
  3. 客户端 → 服务器:ACK(确认)

四次挥手

  1. 客户端 → 服务器:FIN(请求关闭)
  2. 服务器 → 客户端:ACK(确认收到)
  3. 服务器 → 客户端:FIN(请求关闭)
  4. 客户端 → 服务器:ACK(确认收到)

为什么挥手多一次

  • TCP 是全双工通信,两个方向的数据传输相互独立
  • 客户端发送 FIN 表示不再发送数据,但仍可以接收数据
  • 服务器需要先 ACK 确认,然后等待自己的数据发送完毕后,再发送 FIN
  • 因此 ACK 和 FIN 不能合并,需要四次

Q5: 什么是重排(Reflow)和重绘(Repaint)?如何减少重排?

答案

概念触发条件性能影响
重排元素位置、大小改变高(重新计算布局)
重绘元素外观改变(颜色等)中(只需重新绘制)

触发重排的操作

  • 添加/删除 DOM 元素
  • 修改元素尺寸(width、height、padding、margin)
  • 修改元素位置(position、top、left)
  • 获取布局信息(offsetTop、clientWidth、getComputedStyle)

减少重排的方法

// ❌ 频繁重排
for (let i = 0; i < 100; i++) {
element.style.left = i + 'px'; // 100 次重排
}

// ✅ 批量修改样式
element.style.cssText = 'left: 100px; top: 100px;';

// ✅ 使用 class 切换
element.classList.add('moved');

// ✅ 使用 DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
fragment.appendChild(li);
}
list.appendChild(fragment); // 只重排一次

// ✅ 脱离文档流后修改
element.style.display = 'none'; // 脱离文档流
// ... 多次修改 ...
element.style.display = 'block'; // 恢复,只重排一次

// ✅ 使用 transform 代替位置改变
element.style.transform = 'translateX(100px)'; // 不触发重排

Q6: DNS 预解析和预连接对首屏性能的影响

答案

DNS 预解析(dns-prefetch)和预连接(preconnect)是两种资源提示(Resource Hints),可以显著减少首屏加载中的网络延迟。

两者的区别

特性dns-prefetchpreconnect
作用仅完成 DNS 解析DNS 解析 + TCP 连接 + TLS 握手
节省时间20-120ms100-300ms
资源消耗极低中等(维持连接有成本)
浏览器支持非常好
适用场景可能用到的第三方域名确定会用到的关键域名
<!-- DNS 预解析:仅解析域名到 IP -->
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//analytics.example.com">

<!-- 预连接:DNS + TCP + TLS 全流程 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

实际应用的最佳实践

// 动态添加 preconnect(用于 SPA 中的按需预连接)
function addPreconnect(url: string): void {
// 避免重复添加
const existing = document.querySelector(`link[rel="preconnect"][href="${url}"]`);
if (existing) return;

const link = document.createElement('link');
link.rel = 'preconnect';
link.href = url;
link.crossOrigin = 'anonymous';
document.head.appendChild(link);
}

// 结合其他资源提示
function optimizeResourceLoading(): void {
// 1. 关键 API 域名 - 用 preconnect
addPreconnect('https://api.example.com');

// 2. CDN 域名 - 用 preconnect
addPreconnect('https://cdn.example.com');

// 3. 可能用到的第三方 - 用 dns-prefetch
const prefetch = document.createElement('link');
prefetch.rel = 'dns-prefetch';
prefetch.href = '//analytics.example.com';
document.head.appendChild(prefetch);
}
注意事项
  1. preconnect 不要滥用,建议不超过 4-6 个域名,否则连接维护成本会抵消收益
  2. 如果连接建立后 10 秒内没有使用,浏览器会自动关闭连接,造成资源浪费
  3. dns-prefetch 成本很低,可以多配几个;preconnect 成本较高,只用于关键域名
  4. 跨域请求需要添加 crossorigin 属性,否则实际请求时会重新建立连接

Q7: HTTP/2 的多路复用如何改变了资源加载策略?

答案

HTTP/2 的多路复用(Multiplexing)允许在同一个 TCP 连接上并行发送多个请求和响应,彻底解决了 HTTP/1.1 的队头阻塞问题,从而改变了许多传统的前端优化策略。

HTTP/1.1 vs HTTP/2 的区别

HTTP/2 带来的优化策略变化

传统策略(HTTP/1.1)HTTP/2 下是否还需要原因
雪碧图(CSS Sprites)不再必要多路复用下多个小图请求无额外开销
文件合并(concat)不再必要多文件并行传输,合并反而影响缓存粒度
域名分片(domain sharding)反而有害多域名导致多个 TCP 连接,无法利用多路复用
内联资源(inline CSS/JS)按需使用小文件可保持独立以利用缓存
代码分割(code splitting)更加重要粒度更细的分割能充分利用多路复用和缓存
// HTTP/1.1 时代 - 需要合并请求减少连接数
// ❌ 在 HTTP/2 下不再必要
async function fetchAllDataHTTP1(): Promise<void> {
// 合并多个请求为一个,减少 HTTP 连接数
const response = await fetch('/api/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requests: ['/users', '/posts', '/comments'],
}),
});
const { users, posts, comments } = await response.json();
}

// HTTP/2 时代 - 可以直接并行请求
// ✅ 多路复用下多个请求共用一个 TCP 连接
async function fetchAllDataHTTP2(): Promise<void> {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
fetch('/api/comments').then((r) => r.json()),
]);
}

HTTP/2 服务端推送(Server Push):

// 服务端可以主动推送客户端可能需要的资源
// Express + spdy/http2 示例
import http2 from 'http2';

function handleRequest(
stream: http2.ServerHttp2Stream,
headers: http2.IncomingHttpHeaders
): void {
// 当客户端请求 index.html 时,主动推送 CSS 和 JS
if (headers[':path'] === '/index.html') {
// 推送 CSS
stream.pushStream({ ':path': '/styles.css' }, (err, pushStream) => {
if (!err) {
pushStream.respond({ ':status': 200, 'content-type': 'text/css' });
pushStream.end('body { margin: 0; }');
}
});
}
}
HTTP/2 优化建议
  1. 细粒度代码分割:利用多路复用,将代码分割为更小的 chunk,提升缓存命中率
  2. 减少域名数量:将资源集中到同一域名下,充分利用多路复用
  3. 使用 preload:提前告知浏览器关键资源,配合 HTTP/2 并行加载效果更佳
  4. 保持独立文件:不再需要合并文件,独立文件有更好的缓存策略

Q8: 白屏时间和首屏时间的区别,如何优化?

答案

白屏时间(FP/FCP)和首屏时间(LCP)是衡量页面加载速度的两个核心指标,但它们关注的时间节点不同。

指标英文含义触发时机
白屏时间FP(First Paint)浏览器开始渲染第一个像素页面不再是白色
首次内容绘制FCP(First Contentful Paint)首次渲染文本、图片等内容用户看到"有内容"
最大内容绘制LCP(Largest Contentful Paint)最大可见内容元素渲染完成用户感知"加载完成"
首屏时间-首屏所有内容渲染完毕无需滚动即可看到完整内容

测量方法

// 使用 Performance API 测量
function measurePageTiming(): void {
// 1. 白屏时间 (FP)
const fpEntry = performance.getEntriesByName('first-paint')[0];
if (fpEntry) {
console.log(`白屏时间 (FP): ${fpEntry.startTime.toFixed(2)}ms`);
}

// 2. 首次内容绘制 (FCP)
const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0];
if (fcpEntry) {
console.log(`首次内容绘制 (FCP): ${fcpEntry.startTime.toFixed(2)}ms`);
}

// 3. 最大内容绘制 (LCP)
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(`最大内容绘制 (LCP): ${lastEntry.startTime.toFixed(2)}ms`);
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });

// 4. 使用 Navigation Timing API 计算关键时间
window.addEventListener('load', () => {
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;

console.log({
DNS: timing.domainLookupEnd - timing.domainLookupStart,
TCP: timing.connectEnd - timing.connectStart,
TTFB: timing.responseStart - timing.requestStart,
DOM解析: timing.domInteractive - timing.responseEnd,
DOMContentLoaded: timing.domContentLoadedEventEnd - timing.fetchStart,
完全加载: timing.loadEventEnd - timing.fetchStart,
});
});
}

白屏时间优化方案(目标:减少 FP/FCP 时间):

// 1. 内联关键 CSS - 避免阻塞首次渲染
// 在 HTML head 中内联首屏关键样式
const criticalCSS = `
<style>
body { margin: 0; font-family: system-ui; }
.header { height: 64px; background: #fff; }
.hero { min-height: 400px; }
.skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); }
</style>
`;

// 2. 骨架屏 - 让用户感知到页面在加载
// 在 HTML 中直接放入骨架屏,不依赖 JS
const skeletonHTML = `
<div id="root">
<div class="skeleton header"></div>
<div class="skeleton hero"></div>
</div>
`;
<!-- 3. 资源加载优化 -->
<head>
<!-- 内联关键 CSS -->
<style>/* critical CSS */</style>

<!-- 预连接关键域名 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.js" as="script">

<!-- 异步加载非关键 CSS -->
<link rel="preload" href="/non-critical.css" as="style" onload="this.rel='stylesheet'">

<!-- defer 加载 JS,不阻塞 HTML 解析 -->
<script defer src="/app.js"></script>
</head>

首屏时间优化方案(目标:减少 LCP 时间):

策略说明效果
SSR / SSG服务端直出 HTML减少 JS 执行等待时间
代码分割只加载首屏需要的 JS减少主包体积
图片优化WebP/AVIF + 懒加载 + 响应式减少最大元素的加载时间
CDN 加速静态资源 CDN 分发减少网络延迟
缓存策略强缓存 + 协商缓存二次访问秒开
流式渲染React 18 Streaming SSR首字节更快到达
// 图片懒加载 + LCP 优化
function optimizeImages(): void {
// 首屏图片使用 eager loading + fetchpriority
// <img src="hero.jpg" loading="eager" fetchpriority="high" />

// 非首屏图片使用懒加载
// <img src="below-fold.jpg" loading="lazy" />

// 使用 Intersection Observer 实现自定义懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src!;
observer.unobserve(img);
}
});
});

document.querySelectorAll('img[data-src]').forEach((img) => {
observer.observe(img);
});
}
优化优先级
  1. 首先优化白屏:让用户尽快看到内容(骨架屏、内联 CSS、preconnect)
  2. 然后优化首屏:让首屏内容尽快完整展示(SSR、代码分割、图片优化)
  3. 最后优化交互:让页面尽快可交互(TTI 优化、JS 延迟加载)

相关链接