跳到主要内容

首屏优化

问题

如何优化网页首屏加载速度?关键渲染路径是什么?有哪些具体的优化手段?

面试速答版

什么是关键渲染路径? 指浏览器把 HTML/CSS/JS 变成屏幕像素的过程:

  • 流程是 DOM 构建 → CSSOM 构建 → 合并成 Render Tree → Layout → Paint → Composite
  • 关键资源指:阻塞渲染的 HTML、CSS、同步 JS。优化目标是减少关键资源数量、缩小关键资源体积、缩短关键路径长度
  • CSS 默认阻塞渲染(构建 CSSOM),同步 JS 默认阻塞解析,所以减阻塞是首屏优化的核心。

怎么优化首屏加载? 按「网络 → 解析 → 渲染」三段优化:

  • 网络:HTTP/2/3 多路复用、CDN 就近、preconnect/dns-prefetch 提前建连、关键资源 preload
  • 解析:JS 加 defer 不阻塞 HTML 解析、CSS 关键样式内联到 HTML 里、非关键 CSS 用 media 异步加载。
  • 渲染:上 SSR/SSG 让服务端直出 HTML、首屏图加 fetchpriority="high"、骨架屏过渡。
  • 代码层:路由级代码分割(React.lazy + Suspense),首屏 JS 控制在 100KB(gzip)以内。

有哪些具体可落地的手段? 常用清单:

  • 资源:图片用 WebP/AVIF + srcset 响应式、字体加 font-display: swap 防 FOIT、Tree Shaking 去死代码。
  • 加载策略<script defer> 替代 <script>、第三方脚本 async 或延迟到 requestIdleCallback
  • 缓存:合理设 Cache-Control、Service Worker 做预缓存。
  • 指标:用 Lighthouse 或 web-vitals 监控 FCP/LCP,目标 LCP < 2.5s。

答案

首屏优化是前端性能优化的核心,目标是让用户尽快看到页面内容。涉及关键渲染路径优化、资源加载策略、渲染策略等多个方面。


关键渲染路径

关键渲染路径(Critical Rendering Path)是浏览器将 HTML、CSS、JavaScript 转换为像素的过程。

阶段说明优化方向
DOM 构建解析 HTML减少 HTML 大小
CSSOM 构建解析 CSS内联关键 CSS
Render Tree合并 DOM 和 CSSOM减少渲染阻塞
Layout计算元素位置减少重排
Paint绘制像素减少重绘
Composite图层合成使用合成属性

核心指标

指标全称含义目标值
FCPFirst Contentful Paint首次内容绘制< 1.8s
LCPLargest Contentful Paint最大内容绘制< 2.5s
FIDFirst Input Delay首次输入延迟< 100ms
CLSCumulative Layout Shift累积布局偏移< 0.1
TTITime 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
JavaScriptdefer/async,代码分割
Render Tree减少渲染阻塞资源
Layout/Paint避免强制同步布局

Q3: preload、prefetch、preconnect 的区别?

答案

指令优先级用途时机
preload当前页面关键资源立即
prefetch下一页资源空闲时
preconnect建立连接立即
dns-prefetchDNS 解析空闲时
<!-- 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 的优缺点?

答案

对比SSRCSR
首屏速度
SEO差(需处理)
服务器压力
交互响应需 hydration即时
开发复杂度
缓存策略复杂简单

选择建议

  • 内容型网站(博客、新闻)→ SSR/SSG
  • 应用型网站(后台、工具)→ CSR
  • 混合需求 → 部分 SSR + CSR

相关链接