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; } } }); }}