跳到主要内容

首屏优化

问题

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

答案

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


关键渲染路径

关键渲染路径(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

相关链接