首屏优化
问题
如何优化网页首屏加载速度?关键渲染路径是什么?有哪些具体的优化手段?
答案
首屏优化是前端性能优化的核心,目标是让用户尽快看到页面内容。涉及关键渲染路径优化、资源加载策略、渲染策略等多个方面。
关键渲染路径
关键渲染路径(Critical Rendering Path)是浏览器将 HTML、CSS、JavaScript 转换为像素的过程。
| 阶段 | 说明 | 优化方向 |
|---|---|---|
| DOM 构建 | 解析 HTML | 减少 HTML 大小 |
| CSSOM 构建 | 解析 CSS | 内联关键 CSS |
| Render Tree | 合并 DOM 和 CSSOM | 减少渲染阻塞 |
| Layout | 计算元素位置 | 减少重排 |
| Paint | 绘制像素 | 减少重绘 |
| Composite | 图层合成 | 使用合成属性 |
核心指标
| 指标 | 全称 | 含义 | 目标值 |
|---|---|---|---|
| FCP | First Contentful Paint | 首次内容绘制 | < 1.8s |
| LCP | Largest Contentful Paint | 最大内容绘制 | < 2.5s |
| FID | First Input Delay | 首次输入延迟 | < 100ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | < 0.1 |
| TTI | Time to Interactive | 可交互时间 | < 3.8s |
优化策略总览
关键 CSS 内联
// 构建时提取关键 CSS
// vite-plugin-critical
import critical from 'vite-plugin-critical';
export default {
plugins: [
critical({
criticalUrl: 'http://localhost:3000',
criticalPages: [
{ uri: '/', template: 'index' }
],
criticalConfig: {
inline: true,
dimensions: [
{ width: 375, height: 667 }, // 移动端
{ width: 1920, height: 1080 } // 桌面
]
}
})
]
};
<!-- 内联关键 CSS -->
<head>
<style>
/* 关键 CSS,首屏立即渲染 */
.header { ... }
.hero { ... }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
JS 加载优化
defer 和 async
<!-- 阻塞渲染(避免) -->
<script src="app.js"></script>
<!-- 异步加载,乱序执行 -->
<script async src="analytics.js"></script>
<!-- 异步加载,顺序执行,DOMContentLoaded 前 -->
<script defer src="app.js"></script>
代码分割
// React 路由懒加载
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
// Vite 配置分包
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['react', 'react-dom'],
'ui': ['antd', '@ant-design/icons'],
'utils': ['lodash-es', 'dayjs']
}
}
}
}
};
预加载策略
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 预连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://api.example.com">
<!-- 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.webp" as="image">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<!-- 预获取下一页资源 -->
<link rel="prefetch" href="next-page.js">
<!-- 预渲染下一页(谨慎使用) -->
<link rel="prerender" href="https://example.com/next-page">
// 动态预加载
function preloadNextPage(url: string): void {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
}
// 基于用户行为预加载
document.querySelector('.nav-link')?.addEventListener('mouseenter', () => {
preloadNextPage('/next-page.js');
});
骨架屏
// 骨架屏组件
function Skeleton({ width, height, circle = false }: {
width?: string | number;
height?: string | number;
circle?: boolean;
}) {
return (
<div
className="skeleton"
style={{
width,
height,
borderRadius: circle ? '50%' : '4px'
}}
/>
);
}
// 列表骨架屏
function ListSkeleton({ count = 5 }: { count?: number }) {
return (
<div className="list-skeleton">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="item-skeleton">
<Skeleton width={48} height={48} circle />
<div className="content">
<Skeleton width="60%" height={16} />
<Skeleton width="80%" height={14} />
</div>
</div>
))}
</div>
);
}
// 使用
function UserList() {
const { data, isLoading } = useQuery('users', fetchUsers);
if (isLoading) return <ListSkeleton count={10} />;
return (
<ul>
{data.map(user => <UserItem key={user.id} user={user} />)}
</ul>
);
}
/* 骨架屏动画 */
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e8e8e8 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
SSR 服务端渲染
// Next.js SSR
export async function getServerSideProps() {
const data = await fetchData();
return {
props: { data }
};
}
function Page({ data }: { data: DataType }) {
return <div>{/* 渲染数据 */}</div>;
}
// React 18 Streaming SSR
import { renderToPipeableStream } from 'react-dom/server';
import { Suspense } from 'react';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(
<Suspense fallback={<Loading />}>
<App />
</Suspense>,
{
bootstrapScripts: ['/main.js'],
onShellReady() {
res.setHeader('Content-Type', 'text/html');
pipe(res);
}
}
);
});
资源优先级
<!-- 高优先级资源 -->
<link rel="preload" href="hero.webp" as="image" fetchpriority="high">
<img src="hero.webp" fetchpriority="high" alt="Hero">
<!-- 低优先级资源 -->
<img src="below-fold.jpg" fetchpriority="low" loading="lazy" alt="">
<script src="analytics.js" fetchpriority="low" async></script>
// Fetch API 优先级
fetch('/api/critical-data', {
priority: 'high'
});
fetch('/api/secondary-data', {
priority: 'low'
});
避免布局偏移(CLS)
<!-- 始终指定图片尺寸 -->
<img src="image.jpg" width="800" height="600" alt="">
<!-- 使用 aspect-ratio -->
<img
src="image.jpg"
style="aspect-ratio: 16/9; width: 100%;"
alt=""
>
/* 为动态内容预留空间 */
.ad-container {
min-height: 250px;
}
/* 字体加载策略 */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap; /* 或 optional */
}
完整优化清单
const firstScreenOptimizationChecklist = {
// 资源优化
resources: [
'启用 Gzip/Brotli 压缩',
'使用 WebP/AVIF 图片格式',
'压缩和内联关键 CSS',
'代码分割和懒加载',
'移除未使用的代码(Tree Shaking)'
],
// 加载优化
loading: [
'使用 CDN',
'配置强缓存',
'DNS 预解析和预连接',
'预加载关键资源',
'defer/async 脚本'
],
// 渲染优化
rendering: [
'骨架屏或加载指示器',
'SSR/SSG',
'避免布局偏移',
'设置资源优先级'
],
// 监控
monitoring: [
'监控 Core Web Vitals',
'设置性能预算',
'定期 Lighthouse 检测'
]
};
常见面试问题
Q1: 如何优化首屏加载时间?
答案:
// 1. 减少关键资源
// - 内联关键 CSS
// - defer/async JS
// - 代码分割
// 2. 减少资源大小
// - 压缩(Gzip/Brotli)
// - Tree Shaking
// - 图片优化
// 3. 提前加载
// - dns-prefetch
// - preconnect
// - preload 关键资源
// 4. 架构优化
// - SSR/SSG
// - 骨架屏
// - 流式渲染
Q2: 什么是关键渲染路径?如何优化?
答案:
关键渲染路径是浏览器将 HTML、CSS、JS 转换为屏幕像素的步骤。
优化策略:
| 步骤 | 优化方法 |
|---|---|
| DOM | 减少 HTML 嵌套,精简标签 |
| CSSOM | 内联关键 CSS,异步加载非关键 CSS |
| JavaScript | defer/async,代码分割 |
| Render Tree | 减少渲染阻塞资源 |
| Layout/Paint | 避免强制同步布局 |
Q3: preload、prefetch、preconnect 的区别?
答案:
| 指令 | 优先级 | 用途 | 时机 |
|---|---|---|---|
| preload | 高 | 当前页面关键资源 | 立即 |
| prefetch | 低 | 下一页资源 | 空闲时 |
| preconnect | 高 | 建立连接 | 立即 |
| dns-prefetch | 低 | DNS 解析 | 空闲时 |
<!-- preload: 当前页必需 -->
<link rel="preload" href="main.js" as="script">
<!-- prefetch: 下一页可能需要 -->
<link rel="prefetch" href="next.js">
<!-- preconnect: 第三方域名 -->
<link rel="preconnect" href="https://api.example.com">
Q4: 如何避免 CLS(布局偏移)?
答案:
<!-- 1. 图片指定尺寸 -->
<img src="image.jpg" width="800" height="600">
<!-- 2. 使用 aspect-ratio -->
<div style="aspect-ratio: 16/9;">
<img src="image.jpg" style="width: 100%; height: 100%;">
</div>
<!-- 3. 预留广告位空间 -->
<div class="ad-slot" style="min-height: 250px;"></div>
<!-- 4. 字体加载策略 -->
<style>
@font-face {
font-family: 'MyFont';
src: url('font.woff2');
font-display: swap;
}
</style>
<!-- 5. 动态内容使用骨架屏 -->
Q5: SSR 和 CSR 的优缺点?
答案:
| 对比 | SSR | CSR |
|---|---|---|
| 首屏速度 | 快 | 慢 |
| SEO | 好 | 差(需处理) |
| 服务器压力 | 大 | 小 |
| 交互响应 | 需 hydration | 即时 |
| 开发复杂度 | 高 | 低 |
| 缓存策略 | 复杂 | 简单 |
选择建议:
- 内容型网站(博客、新闻)→ SSR/SSG
- 应用型网站(后台、工具)→ CSR
- 混合需求 → 部分 SSR + CSR