首屏白屏时间长而 DOMContentLoaded 快,是因为渲染需等待 CSSOM 构建完成才能生成 Render Tree,而 DOM 解析不受阻塞;关键 CSS 必须内联(≤14KB),非关键 CSS 用 media="print" onload 或动态插入异步加载。

为什么首屏白屏时间长,但 DOMContentLoaded 却很快
因为浏览器渲染不只等 DOM 构建完成——它必须等关键 CSSOM 就绪,才能合成 Render Tree。外部 <link rel="stylesheet"> 会中断 HTML 解析,哪怕 main.css 只有 5KB 且已缓存,仍要走完整验证、下载、解析流程。Network 面板里 Initiated by 列显示 parser,就是明确信号。
常见错误现象:
- 首屏内容写在 HTML 底部,
<h1>出现在</body>前 200 行,LCP 自然延迟 - 在
<head>里用@import引入 CSS,强制串行加载,实测拖慢 FCP 300ms+ - 把整站样式打包进单个
app.css,非首屏模块(如后台表格、弹窗)的规则白白阻塞渲染
怎么提取真正“关键”的 CSS 规则
关键 CSS 不是体积最小的那部分,而是首屏像素生成所必需的最小样式集合。Coverage 面板只能看字节覆盖率,但伪类、媒体查询是否触发取决于设备视口和交互状态,不能全信。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 本地开发用
puppeteer + critters:访问http://localhost:3000,抓取真实首屏 DOM 快照,反向计算被用到的选择器 - Vite 用户直接配
vite-plugin-critical,build 时自动提取并注入<style>到index.html - 内联体积控制在 ≤14KB:超过 TCP 初始拥塞窗口,会拆包,反而延长首字节到达时间(TTFB)
- 禁用
@import:浏览器会同步拉取被导入文件,破坏内联意义
非关键 CSS 怎么延迟加载又不破坏响应式逻辑
不能删,也不能放任它阻塞。关键是让浏览器先跳过,等首屏稳定后再触发。
推荐做法:
- 用
media="print"初始化:<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">——print 媒体默认不加载,onload 触发后才切换,避免 FOUC - 更稳妥的是 DOMContentLoaded 后动态插入:
const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'non-critical.css'; document.head.appendChild(link); - 对条件加载场景(如仅桌面端需要的布局),直接用
media="(min-width: 768px)",浏览器原生支持按需加载
HTML 结构本身怎么成为性能瓶颈
嵌套过深不是“看起来乱”,而是实实在在拖慢解析。浏览器流式解析每层 <div> 都要创建节点、计算样式、可能触发 Layout。超 6 层嵌套,低端设备上 FCP 明显延迟。
容易踩的坑:
- 用
<table>套<div>:表格单元格内样式计算本就重,再加嵌套极易触发同步 Layout - 在
<head>里用 JS 动态插入首屏 DOM:等于放弃浏览器原生流式解析,退化成 JS 执行后重建 - 首屏内容(
<img fetchpriority="high">、<form>)写在 HTML 底部:浏览器不会“跳着解析”,必须等前面所有标签走完 - 导航栏这类非首屏内容,可考虑
loading="lazy"+fetchpriority="low"降权,或拆成异步<template>+ JS 插入
真正难的是结构优化没法一劳永逸——改完一个页面的嵌套,可能暴露另一个页面的 CRP 断点,得持续用 DevTools 的 Elements 面板右键 → “Show DOM properties” 查 depth 值来盯。



















