1906 字
10 分钟
首屏加载优化

首屏加载优化#

首屏加载时间是衡量用户体验的关键指标。本文从资源加载、渲染优化、网络优化等多个维度,深入探讨提升首屏性能的实战方案。

资源加载优化#

路由懒加载#

通过动态导入实现按需加载,减少首屏 bundle 体积。

const routes = [
{
path: '/',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
]

代码分割#

Webpack 默认支持代码分割,将代码按需加载,提升首屏性能。

module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 第三方库单独打包
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
// 公共模块提取
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
}

动态导入

button.addEventListener('click', async () => {
const module = await import('./heavyModule.js');
module.doSomething();
})

资源压缩#

打包时使用压缩插件,可显著减少文件体积。推荐使用 Gzip 或 Brotli 压缩。

// Vite 配置
import viteCompression from 'vite-plugin-compression'
export default {
plugins: [
// Gzip 压缩
viteCompression({
algorithm: 'gzip',
ext: '.gz'
}),
// Brotli 压缩(压缩率更高)
viteCompression({
algorithm: 'brotliCompress',
ext: '.br'
})
]
}

图片优化#

图片通常占据页面资源的大部分体积,优化图片对首屏性能至关重要。

<!-- 懒加载 -->
<img loading="lazy" src="image.jpg" alt="description">
<!-- 响应式图片 -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="description">
</picture>
<!-- 使用 CDN -->
<img src="https://cdn.example.com/image.jpg" alt="description">

渲染优化#

SSR(服务端渲染)#

服务器渲染将页面渲染成 HTML,然后发送给客户端,可大幅提升首屏渲染速度。

// Next.js 示例
export async function getServerSideProps() {
const data = await fetch('https://api.example.com/data')
return { props: { data } }
}
// Nuxt.js 示例
export default {
async asyncData() {
const data = await fetch('https://api.example.com/data')
return { data }
}
}

预渲染 / 静态生成#

将页面预先渲染成静态 HTML 文件,适用于内容不频繁变化的页面。

// Next.js 静态生成
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data')
return {
props: { data },
revalidate: 60 // ISR: 60秒后重新生成
}
}

骨架屏#

在页面加载时显示骨架屏,提升用户感知性能。

<template>
<div v-if="loading">
<skeleton-screen />
</div>
<div v-else>
<actual-content :data="data" />
</div>
</template>

网络优化#

CDN + 缓存#

CDN 缓存可以将静态资源上传到 CDN,然后设置合理的缓存时间。HTTP 缓存通过设置 Cache-Control 头来控制资源缓存策略。

Nginx 配置示例

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

预加载 / 预连接#

通过资源提示优化资源加载时机,减少网络延迟。

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://example.com/">
<!-- 预连接 -->
<link rel="preconnect" href="https://example.com/">
<!-- 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="font.woff2" as="font" crossorigin>
<!-- 预先获取下一页的资源 -->
<link rel="prefetch" href="/next-page.js">

HTTP/2 & HTTP/3#

HTTP/2 是新一代网络协议,相比 HTTP/1.1 有显著性能提升。HTTP/3 则是基于 QUIC 协议的最新版本。

主要特性:

  • 多路复用:在一个 TCP 连接中并行处理多个请求,避免队头阻塞
  • 服务器推送:服务器可以主动推送资源,减少客户端请求次数
  • 头部压缩:使用 HPACK 算法压缩 HTTP 头部,减少传输开销

注意:HTTP/2 和 HTTP/3 需要在 HTTPS 环境下使用。

事件优化#

防抖(Debounce)#

在事件被触发 n 毫秒后再执行回调,如果在 n 毫秒内事件被再次触发,则重新计时。

适用场景:

  • 搜索框输入,只执行最后一次输入
  • 窗口 resize,只执行最后一次 resize
// 基础版本
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 清除之前的定时器
clearTimeout(timer);
// 重新设置定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用示例
const search = debounce((keyword) => {
console.log('搜索关键字为:', keyword);
}, 500);
input.addEventListener('input', (e) => {
search(e.target.value);
});

立即执行版本

function debounce(fn, delay, immediate = false) {
let timer = null;
return function(...args) {
const callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) {
fn.apply(this, args);
}
}, delay);
if (callNow) {
fn.apply(this, args);
}
};
}

节流(Throttle)#

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

适用场景:

  • 鼠标移动事件
  • 页面滚动事件
// 时间戳版本
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
}
// 使用示例
const handleScroll = throttle(() => {
console.log('滚动了');
}, 200);
window.addEventListener('scroll', handleScroll);

定时器版本

function throttle(fn, delay) {
let timer = null;
return function(...args) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}

结合版本(首次立即执行 + 最后一次保证执行)

function throttle(fn, delay) {
let timer = null;
let lastTime = 0;
return function(...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
clearTimeout(timer);
if (remaining <= 0) {
// 立即执行
lastTime = now;
fn.apply(this, args);
} else {
// 保证最后一次执行
timer = setTimeout(() => {
lastTime = Date.now();
fn.apply(this, args);
}, remaining);
}
};
}

虚拟列表#

问题场景#

渲染 10 万条数据时,DOM 节点过多导致页面卡顿。

核心思路#

只渲染可视区域的数据,通过滚动时动态替换内容来提升性能。

原生 JavaScript 实现#

class VirtualList {
constructor({
container, // 容器元素
itemHeight, // 每项高度
data, // 数据
renderItem // 渲染函数
}) {
this.container = container;
this.itemHeight = itemHeight;
this.data = data;
this.renderItem = renderItem;
// 可视区域的高度
this.viewHeight = container.clientHeight;
// 可视区域可显示的数量
this.visibleCount = Math.ceil(this.viewHeight / this.itemHeight);
// 缓存区数量
this.bufferCount = 3;
this.init();
}
init() {
// 创建容器
this.listContainer = document.createElement('div');
this.listContainer.style.height = `${this.data.length * this.itemHeight}px`;
this.listContainer.style.position = 'relative';
this.container.appendChild(this.listContainer);
this.container.addEventListener('scroll', () => this.onScroll());
this.render();
}
onScroll() {
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
// 计算可视区域的起始索引
const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.bufferCount);
// 计算可视区域的结束索引
const endIndex = Math.min(this.data.length, startIndex + this.visibleCount + this.bufferCount);
// 清空容器
this.listContainer.innerHTML = '';
// 渲染可见项
for (let i = startIndex; i < endIndex; i++) {
const item = this.renderItem(this.data[i], i);
item.style.position = 'absolute';
item.style.top = `${i * this.itemHeight}px`;
item.style.height = `${this.itemHeight}px`;
this.listContainer.appendChild(item);
}
}
}
// 使用示例
// 创建 10 万条数据
const data = Array.from({ length: 100000 }, (_, i) => ({
id: i,
text: `Item ${i}`
}));
// 初始化虚拟列表
new VirtualList({
container: document.querySelector('#container'),
itemHeight: 50,
data: data,
renderItem: (item) => {
const div = document.createElement('div');
div.textContent = item.text;
div.style.border = '1px solid #ccc';
return div;
}
});

Vue 实现#

<template>
<div
ref="container"
class="virtual-list"
@scroll="onScroll"
>
<div
class="list-phantom"
:style="{ height: totalHeight + 'px' }"
></div>
<div
class="list-content"
:style="{ transform: `translateY(${offset}px)` }"
>
<div
v-for="item in visibleData"
:key="item.id"
class="list-item"
:style="{ height: itemHeight + 'px' }"
>
{{ item.text }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
data: Array,
itemHeight: { type: Number, default: 50 }
})
const container = ref(null)
const scrollTop = ref(0)
const viewHeight = ref(600)
const bufferCount = 3
// 可见数量
const visibleCount = computed(() =>
Math.ceil(viewHeight.value / props.itemHeight)
)
// 总高度
const totalHeight = computed(() =>
props.data.length * props.itemHeight
)
// 起始索引
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - bufferCount)
)
// 结束索引
const endIndex = computed(() =>
Math.min(props.data.length, startIndex.value + visibleCount.value + bufferCount * 2)
)
// 可见数据
const visibleData = computed(() =>
props.data.slice(startIndex.value, endIndex.value)
)
// 偏移量
const offset = computed(() =>
startIndex.value * props.itemHeight
)
const onScroll = (e) => {
scrollTop.value = e.target.scrollTop
}
</script>
<style scoped>
.virtual-list {
height: 600px;
overflow: auto;
position: relative;
}
.list-phantom {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.list-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.list-item {
border: 1px solid #ccc;
display: flex;
align-items: center;
padding: 0 10px;
}
</style>

动态高度优化#

当列表项高度不固定时,需要动态计算每项的位置信息。

class DynamicVirtualList {
constructor(options) {
// ...
this.positions = data.map((_, index) => ({
index,
height: itemHeight, // 预估高度
top: index * itemHeight,
bottom: (index + 1) * itemHeight
}));
}
updatePositions() {
const nodes = this.listContainer.children;
Array.from(nodes).forEach(node => {
const rect = node.getBoundingClientRect();
const index = +node.dataset.index;
const oldHeight = this.positions[index].height;
const newHeight = rect.height;
// 高度有变化,更新后续所有项的位置
if (oldHeight !== newHeight) {
const diff = newHeight - oldHeight;
this.positions[index].height = newHeight;
this.positions[index].bottom = this.positions[index].bottom + diff;
for (let i = index + 1; i < this.positions.length; i++) {
this.positions[i].top = this.positions[i - 1].bottom;
this.positions[i].bottom = this.positions[i].bottom + diff;
}
}
});
}
}
首屏加载优化
https://fuwari.vercel.app/posts/first-screen-optimization/
作者
Lorem Ipsum
发布于
2025-10-23
许可协议
CC BY-NC-SA 4.0