白屏优化
白屏优化
白屏优化思路
关于白屏优化可以将浏览器从输入 URL 到 dom 树绘制完成的不同阶段,分为不同的时间点。
浏览器从输入 URl 到绘制 dom 树完成主要是经历以下几个阶段
- URL 解析和请求发起
- 用户输入 URL → 浏览器补全协议(如 http/https);
- 浏览器检查缓存(DNS 缓存、HTTP 缓存);
- 若无缓存或缓存失效,浏览器发起网络请求。
- DNS 查询
- 将域名解析为 IP 地址;
- 可能经历本地缓存、操作系统缓存、DNS 服务器递归查询等过程。
- 与服务器建立连接
- 如果是 https,若是 HTTPS,先进行 TLS/SSL 握手
- 使用 TCP 三次握手 建立连接
- 浏览器与服务器之间建立起安全的通信通道(如果是 HTTPS)。
- 发送 HTTP 请求
- 浏览器发送 HTTP 请求报文(包含方法、路径、头部、Cookie 等);
- 可能包含请求体(如 POST 请求);
- 发送前可能会经过浏览器的某些优化(如预连接、缓存验证等)。
- 服务器处理请求并返回响应(状态码、响应头、响应体);
- 响应体可能是 HTML、JSON、图片、重定向等资源。
- 浏览器接收响应并开始渲染
- 构建 DOM 树
- HTML 被解析为 DOM 节点,形成一颗 DOM 树;
- 遇到
<script>
可能会阻塞 DOM 解析,除非有 async 或 defer。
- 构建 CSSOM 树(CSS Object Model)
- 解析所有 CSS 文件、内联样式、style 标签;
- 和 DOM 合并后用于渲染页面。
- 构建 DOM 树
- 构建渲染树(Render Tree)
- 浏览器将 DOM 和 CSSOM 合并,生成 Render Tree;
- Render Tree 是包含可见元素的视觉树结构(如 display: none 的元素不会出现在里面)。
- 布局(Layout / Reflow)
- 绘制
- 将每个渲染节点转化为实际像素;
- 合成(Compositing)
graph TD
A[用户输入URL] --> B[DNS解析]
B --> C[建立TCP连接]
C --> D[发送HTTP请求]
D --> E[接收响应]
E --> F[解析HTML -> DOM树]
E --> G[解析CSS -> CSSOM树]
F --> H[构建渲染树Render Tree]
G --> H
H --> I[布局Layout]
I --> J[绘制Paint]
J --> K[合成Compositing]
K --> L[显示到屏幕]
根据以上几个阶段,分为不同阶段
- 请求发出前
尽快开始加载页面(DNS 预解析、预连接、CDN 加速、压缩资源)
- 资源加载中
加快加载资源(首屏资源预加载、懒加载非核心资源、Skeleton 骨架屏、服务端渲染 SSR/SSG)
- JS 执行中
提高执行效率 (代码分包、按需加载、异步组件、异步路由、减少主线程阻塞、减少第三方库体积)
- 用户感知
减少白屏感知(加载动画、骨架屏、Loading 占位、预渲染、首屏内容 SSR)
详细分类为以下几个层面
- 网络层面
- DNS 预解析
- TCP/SSL 预连接
- CDN 加速
- GZip 压缩
- 图片懒加载
- 资源优化
- 首屏关键 CSS 内联 (减少请求,避免 css 阻塞渲染)
- 代码分包(避免一次性加载)
- 异步加载第三方 SDK
- 构建过程中进行 tree-shaking
- http/2 多路复用(同时加载多个资源,提高加载效率)
- 渲染层优化
- Skeleton Screen(骨架屏)预占位,让用户感知更早渲染
- SSR(服务端渲染)提前生成 HTML 内容,避免等待 JS 执行才能看到内容
- SSG(静态站点生成)对静态内容预编译输出,速度更快(Next.js / Nuxt 支持)
- 页面预渲染 prerender 针对 SPA 首屏页面预渲染,生成静态 HTML
- hydrate 异步组件 SSR 后,延迟 hydrate 次要模块,避免主线程被阻塞
- 用户感知优化
- Loading 动画 给用户立即反馈,减少“白屏”心理压力
- 占位图/预加载图片 首屏图片提前加载或占位
- 延迟加载不重要模块 如底部组件、客服系统等
- 接入缓存系统(如 SW)使用 Service Worker 提供离线或缓存回源体验
- 构建层优化
- 使用现代构建工具(Vite / Webpack 5)提供更快构建、更小 bundle
- 压缩混淆代码 减少文件体积,保护代码逻辑
- 使用现代 JS 语法(esbuild)浏览器支持范围内使用现代语法,提升性能
网络层面优化
DNS 预解析(dns-prefetch)
DNS 类似电话簿,他负责将我们输入的域名转换为计算机能够识别的 IP 地址
DNS 是将域名解析为 IP 地址的系统,是互联网通信的“导航员”。
在浏览器输入域名 URL 后,发生以下几步

DNS 预解析(dns-prefetch),通过在 head 中添加以下标签
<link rel="dns-prefetch" href="//example.com" />
<!-- rel="dns-prefetch":表示要进行 DNS 预解析; -->
<!-- href="//example.com":目标域名(注意不要带协议); -->
<!-- 作用于 https://example.com 的资源加载(如图片、脚本、字体等)。 -->
<!-- 如下列操作 -->
<head>
<!-- DNS 预解析第三方 CDN 域名 -->
<link rel="dns-prefetch" href="//cdn.jsdelivr.net" />
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//www.googletagmanager.com" />
</head>
<!-- 会让浏览器在加载 HTML 时立刻发起 DNS 查询,而不是等到你真的加载对应脚本或字体才解析。 -->
TCL/SSL 预连接(preconnect)
对于需要加载来自目标来源的资源时,可以抢先执行部分或者全部握手,预先连接服务器从而加快未来从目标源加载资源的速度。
<!-- 使用preconnect标记 -->
<link rel="preconnect" href="https://example.com" />
<link rel="preconnect">
将为未来的跨源 HTTP 请求、导航或子资源带来好处。它对同源请求没有好处,因为连接已经打开。
CDN 加速
CDN(内容分发网络),是一种将网站内容缓存在多个地理位置分布的服务器节点(边缘节点)上,并将用户请求就近分配到最快的节点上进行加速的处理技术。
CDN 类似于京东的物流仓,把网站内容提前发送到地理位置周边的服务器,从而使拿到资源的速度更快。
graph TD
A[用户浏览器] --> B[智能 DNS / 调度系统<br>(基于 IP 定位用户)]
B --> C[最近的 CDN 节点响应]
C --> D[CDN 边缘缓存服务器<br>(你附近)]
D --> E{是否命中缓存?}
E -- 是 --> F[直接返回缓存内容]
E -- 否 --> G[回源到网站源服务器<br>(原始内容)]
使用 CDN 加速,将静态资源缓存,这样可以降低服务器的带宽,可用性高,失效率降低。
GZip 代码压缩
Gzip 压缩一种在服务端对资源进行压缩在发送,减小资源的体积,提升加载速度。
关闭 Gzip
开启 Gzip
通过压缩代码体积,减少网络传输数据量,从而加快资源请求时间,减少白屏时间
在项目中多用 Nginx 作为服务端代理,在 Nginx 中使用 Gzip 主要是进行以下配置
指令 | 类型 | 默认值 | 作用 |
---|---|---|---|
gzip | off/on | off | 开启或关闭 gzip |
gzip_comp_level | 1-9 | 1 | 压缩等级,默认为 1,数字越大代表压缩等级越高,但是 CPU 占用也越高,所以不是压缩等级一定响应越快,应该结合服务器配置选择一个合理的范围 |
gzip_types | MIME 列表 | 无 | 指定哪些 MIME 类型的响应启用 Gzip(默认只压缩 text/html) |
gzip_min_length | 字节数 | 0 | 最小压缩长度,响应体小于该值不启用压缩 |
gzip_buffers | 数组 | 32 4k / 16 8k | 设置用于压缩响应的缓冲区数量和大小 |
gzip_http_version | 1.0/1.1 | 1.1 | 限制 Gzip 生效的 HTTP 协议版本 |
gzip_proxied | 值列表 | off | 指定哪些代理请求可以被压缩( expired no-cache no-store private auth;) |
gzip_disable | 条件 | - | 禁用 Gzip 的条件表达式(通常用于旧浏览器) |
gzip_vary | on/off | off | 设置是否发送 Vary: Accept-Encoding 响应头(建议开启) |
gzip_static | on/off | off | 是否使用已生成的 .gz 文件(用于构建阶段生成 gzip 资源时) |
图片懒加载
图片懒加载就是在资源进入可视区域时候再加载,而不是一次性全部加载。
图片在加载图片的时候会发起额外的 http 请求,图片量很多的时候,渲染时间会被拖慢。
使用懒加载可以优先渲染结构,延迟加载图片,加快首屏显示。
实现图片懒加载主要有两种方式
- 使用浏览器原生的 lazy 属性
<img src="example.jpg" loading="lazy" alt="示例图片">
使用 lazy 的优势是无需 js 控制,浏览器自动处理
但是对于旧版本浏览器(IE,某些老版本 safari)存在兼容性问题 - 使用 js 控制,替换
通过 observer 对 dom 节点进行观察,当节点进入可视区域的时候进行 src 的替换,进行加载
/**
* 适用于更复杂的懒加载逻辑,动画,图片替换,预加载
*/
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.classList.remove('lazy')
obs.unobserve(img)
}
})
})
document.querySelectorAll('img.lazy').forEach((img) => {
observer.observe(img)
})
资源优化
首屏关键 CSS 内联 (减少请求,避免 css 阻塞渲染)
将首屏所需要的关键 css 直接写入<head>
中,而不通过<link>
加载外部 css 文件,目的是减少 Http 请求并避免 css 阻塞页面渲染。
浏览器在构建 dom 的时候遇到<link ref="stylesheet">
的时候会暂停 HTML 的渲染,然后去请求外部的 css,等待 css 下载完成并且解析为 cssom 才能继续渲染页面。也就是说外部的 css 会阻塞页面的渲染,导致白屏时间变长。
代码分包(避免一次性加载)
避免一次性加载整个应用的所有代码,只加载当前页面或当前功能所需的模块,降低首屏加载的体积。
现在常用的项目构建工具 vite,webpack 都支持分包。
以 vite 为例,使用 vite+vue3 创建一个新工程。
npm create vite@latest
启用分包

关闭分包


主包体积过大,意味着首屏加载需要更多的数据传输


在 vite 中可以通过 rollup 的配置自定义分包策略

使用 vue 或者 react,使用路由懒加载,在构建时,会进行自动分包
// Vue Router 懒加载写法
const Home = () => import('@/views/Home.vue')
const About = () => import('@/views/About.vue')
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// react 中使用lazy
// Suspense 包裹 loader
const withSuspense = (component: React.ReactNode) => (
<Suspense fallback={<div>加载中...</div>}>{component}</Suspense>
)
const My = lazy(() => import('@/pages/Mobile/My'))
const routes: RouteObject[] = [
{
id: 'my',
handle: { isTabbar: true },
path: 'my',
element: withSuspense(<My />),
},
]
在 webpack 和 vite 中,懒加载都会进行自动分包
webpack 可以通过配置自定义分包策略
module.exports = {
//
optimization: {
splitChunks: {
chunks: 'async', // 只对异步 import 的模块分包(默认值)
minSize: 20000, // 被分包的模块最小体积(单位:字节,20KB)
minRemainingSize: 0, // 输出 chunk 剩余体积限制
minChunks: 1, // 模块至少被引用 1 次才会被抽离
maxAsyncRequests: 30, // 异步加载时并行请求的最大数量
maxInitialRequests: 30, // 初始加载时并行请求的最大数量
enforceSizeThreshold: 50000, // 强制拆包阈值,超过该体积的 chunk 会被进一步拆分
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 中的模块
priority: -10, // 优先级,值越大越优先
reuseExistingChunk: true, // 是否复用已有 chunk
},
default: {
minChunks: 2, // 被至少两个模块引用才抽出为 common chunk
priority: -20,
reuseExistingChunk: true,
},
}
},
}
}
异步加载第三方 SDK
许多SDK体积大,加载慢,和首屏加载无关
在需要的时候再进行加载
// 封装加载SDK函数
function loadSdk(src: string): Promise<void> {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) return resolve()
const script = document.createElement('script')
script.src = src
script.async = true
script.onload = () => resolve()
script.onerror = () => reject(new Error(`SDK 加载失败: ${src}`))
document.head.appendChild(script)
})
}
在首页引入谷歌分析SDK
一项很耗时的操作
构建过程中进行 tree-shaking
Tree Shaking(摇树优化)是一种删除未使用代码的构建优化技术,它在打包阶段会“摇掉”你导入但未使用的代码。减小包的体积
vite/webpack中默认支持树摇
http/2 多路复用(同时加载多个资源,提高加载效率)
HTTP/2 多路复用指的是:一个 TCP 连接上可以同时并发发送多个请求和响应,互不阻塞,共享同一个连接。
协议特性对比表
特性 | HTTP/1.1 | HTTP/2 |
---|---|---|
请求并发 | ❌ 同一连接一次只能处理一个请求 | ✅ 同一连接支持多个并发请求(多路复用) |
队头阻塞(Head-of-line) | ✅ 存在,前一个响应未完成,后续必须等待 | ✅ 消除,多个响应可以交错返回 |
连接数量限制 | ❌ 每个域名通常 6 个连接(浏览器限制) | ✅ 一个域名只需一个连接 |
请求格式 | 文本格式,基于 ASCII | 二进制帧格式,更高效 |
头部大小 | 大,重复,未压缩 | 使用 HPACK 算法压缩,减少冗余 |
多资源加载优化 | 需手动合并文件(JS/CSS)、雪碧图等技巧 | 不需要合并,多个资源可并行加载 |
服务器推送(Server Push) | ❌ 不支持 | ✅ 可预推送资源(已被多数浏览器废弃) |
安全要求(HTTPS) | 非强制 | ✅ 浏览器默认仅在 HTTPS 下启用 |
浏览器支持 | ✅ 全支持 | ✅ 已广泛支持,主流浏览器默认启用 |
开启Http2
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/private.key;
# ... 其他配置
}
但是并不是所有场景都适合http2,因为http2所有请求公用一条连接,一旦出现丢包,阻塞的情况,所有请求都会收到影响
渲染层优化
Skeleton Screen(骨架屏)预占位,让用户感知更早渲染
骨架屏是一种模拟页面真实结构的“加载占位图”,通过灰色块、动画等元素替代真实内容,在页面数据尚未加载完成前就展示结构轮廓,从而显著缓解白屏体验。通过色块伪装,避免纯白页面,增强反馈感觉。
SSR(服务端渲染)提前生成 HTML 内容,避免等待 JS 执行才能看到内容
服务端渲染(Server-Side Rendering)是一种在服务端就将 HTML 页面内容生成并返回给浏览器的技术,让用户在未加载 JS 的情况下就能看到可视页面,有效减少白屏时间。
✅ SSR 能解决什么问题?
问题场景 | SSR 的优化效果 |
---|---|
首屏白屏时间长 | 浏览器收到 HTML 就能渲染出页面,不需等待 JS 执行 |
JS 文件体积大 / 异步加载慢 | SSR 不依赖 JS 执行,内容提前输出 |
SEO 无法抓取 SPA 页面内容 | SSR 输出完整 HTML,提升搜索引擎收录效率 |
慢网环境下用户无法快速看到结构 | SSR 渲染优先,用户能更快“看到内容” |
🔁 SSR vs CSR 对比
特性 | CSR(客户端渲染) | SSR(服务端渲染) |
---|---|---|
内容生成时机 | 浏览器下载 JS 后动态渲染 | 服务端预先生成 HTML,首屏立即可见 |
白屏时间 | ⏱ 较长,需等待 JS 加载 + 执行 | ✅ 几乎没有白屏,HTML 可立即渲染 |
SEO 友好 | ❌ 差,搜索引擎抓不到 SPA 内容 | ✅ 好,内容在 HTML 中 |
首屏性能 | ❌ 慢,依赖 JS 初始化 | ✅ 快,HTML+CSS 立即渲染 |
JS 错误影响 | ❌ 直接导致页面空白 | ✅ 初始页面已显示,不完全依赖 JS |
内容交互 | ✅ 快速、灵活 | ❌ 初始加载后需 hydrate 才具备交互能力 |
如何实现 SSR
项目:
- 使用框架:Nuxt.js
- 内置 SSR 支持,开箱即用
- 页面首次由服务端渲染,后续页面由客户端处理
React 项目:
- 使用框架:Next.js
- 支持 SSR、SSG、ISR 多种渲染方式
- 可通过
getServerSideProps()
动态生成页面内容
SSG(静态站点生成)对静态内容预编译输出,速度更快(Next.js / Nuxt 支持)
静态站点生成(Static Site Generation,SSG)是在构建阶段提前将网站页面编译成纯静态 HTML 文件,部署到 CDN 或静态服务器,访问时直接返回预渲染好的页面,从而实现极快的加载速度和良好的 SEO。
典型支持 SSG 的框架
框架 | SSG 支持方式 | 备注 |
---|---|---|
Next.js | next export / getStaticProps | 支持静态导出,结合 ISR |
Nuxt.js | nuxt generate | 生成静态站点,适合内容站 |
Gatsby | 基于 React 的静态站点生成框架 | 生态丰富,数据源多样 |
Hugo / Jekyll | 纯静态站点生成器 | 适合博客、文档 |
页面预渲染 prerender 针对 SPA 首屏页面预渲染,生成静态 HTML
预渲染是一种静态生成技术,专门针对单页应用(SPA)将首屏页面提前渲染成静态 HTML 文件,在构建时生成,部署后访问时直接返回预渲染好的 HTML,大幅减少白屏时间,提升首屏体验。
✅ 预渲染的核心优势
优势 | 说明 |
---|---|
降低首屏白屏 | 浏览器收到预渲染的 HTML,页面快速可见 |
保持 SPA 架构 | 页面仍是 SPA,交互由客户端激活,不破坏前端逻辑 |
SEO 友好 | 静态 HTML 易被搜索引擎抓取 |
构建简单 | 不依赖服务器动态渲染,适合静态托管 |
Vite 中使用 vite-plugin-prerender-spa
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import prerender from 'vite-plugin-prerender-spa'
export default defineConfig({
plugins: [
vue(),
prerender({
routes: ['/', '/about'],
renderAfterDocumentEvent: 'render-event',
}),
],
})
hydrate 异步组件 SSR 后,延迟 hydrate 次要模块,避免主线程被阻塞
延迟 Hydrate 异步组件:SSR 后优化主线程性能,提升首屏交互
SSR 预渲染页面后,浏览器需要执行 Hydrate 过程把静态 HTML 变成可交互的应用。为了避免主线程被长时间阻塞,可以优先 hydrate 关键组件,延迟异步加载的次要模块,降低白屏和卡顿。
- 背景介绍
- SSR 服务端先渲染好完整 HTML,浏览器直接展示内容,解决白屏问题;
- Hydrate 过程是将服务端渲染的静态 DOM 绑定事件和状态,恢复客户端交互能力;
- 大型应用 Hydrate 阶段很重,耗时长,阻塞主线程,导致界面卡顿,影响用户体验。
- 延迟 Hydrate 的核心思路
关键点 | 说明 |
---|---|
先同步 hydrate 关键模块 | 如导航栏、首屏内容,保证页面快速可交互 |
延迟 hydrate 次要模块 | 非首屏、下方列表、广告等异步组件可延后 |
利用动态导入 + 懒加载 | 异步加载和 hydrate 配合使用 |
结合 requestIdleCallback / setTimeout | 利用空闲时间分批激活次要模块 |
用户感知优化
- Loading 动画 给用户立即反馈,减少“白屏”心理压力
- 占位图/预加载图片 首屏图片提前加载或占位
<link rel="preload" as="image" href="example.jpg">
浏览器会提前请求该图片资源,但不会自动显示;适合关键图片或即将使用的图片预加载;需配合 CSS/JS 使用。 - 延迟加载不重要模块 如底部组件、客服系统等
- 接入缓存系统(如 SW)使用 Service Worker 提供离线或缓存回源体验