1. 首屏优化都有哪些方案和方法,首屏优化加载,是怎么做的?
1. 资源体积优化:
首页应该考虑使用路由懒加载和组件懒加载结合 Suspense 的方法
// 1. 路由懒加载(Vue Router 4)
const routes = [
{
path: '/home',
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')
}
]
// 2. 组件懒加载
const IndexComponent = defineAsyncComponent(() =>
import('@/components/IndexComponent.vue')
)
2. 代码分割策略:
通过 splitChunks 实现代码分割
// webpack配置优化
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'chunk-vendors',
priority: 10
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: 5
}
}
}
}
}
2.1 资源优化
压缩资源:
图片转WebP格式 + 压缩
开启Gzip/Brotli压缩
移除console.log等调试代码
// 2. CDN加速静态资源
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'axios': 'axios'
}
2.2 加载策略优化
<!-- 1. 关键CSS内联 -->
<style>
/* 首屏关键CSS */
.header, .main-content { display: block; }
</style>
<!-- 2. 预加载关键资源 -->
<link rel="preload" href="/fonts/important.woff2" as="font">
<link rel="prefetch" href="/chunk-about.js" as="script">
2.3 渲染优化
骨架屏方案
// 在index.html中放置骨架屏HTML结构
<div id="app">
<div class="skeleton-header"></div>
<div class="skeleton-content"></div>
</div>
通过 v-lazy修饰符 实现图片懒加载
<img v-lazy="imageUrl" alt="description">
3. 实际案例应用:
// 项目中的具体配置
// vue.config.js
module.exports = {
chainWebpack: config => {
// 1. 分包策略
config.optimization.splitChunks({
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20
}
}
})
// 2. 压缩配置
config.plugin('compression').use(CompressionPlugin, [{
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240
}])
},
// 3. 生产环境优化
productionSourceMap: false,
css: {
extract: true,
sourceMap: false
}
}
2. 优化汇总方案:
一、资源优化(减少体积)
1.1 代码优化
- 路由懒加载:按页面拆分chunk,首屏只加载必要代码
- 组件异步加载:非关键组件动态导入
- 第三方库按需引入:Element-UI、Lodash等按需加载
- 代码分割:Webpack SplitChunks优化分包策略
- Tree Shaking:移除未引用代码
1.2 资源压缩
- Gzip/Brotli压缩:服务器开启压缩,减少传输体积
- 图片压缩:WebP格式 + 质量优化 + 尺寸适配
- 代码压缩:移除注释、console.log、变量名缩短(按规范命名变量名)
- 资源合并:雪碧图,精灵图,CSS/JS文件合并
二、网络优化(加速加载)
2.1 协议优化
- HTTP/2:多路复用、头部压缩、服务器推送
- HTTPS:使用HTTP/2必须的加密协议
- QUIC/HTTP3:新一代传输协议(逐步普及)
2.2 加载策略
- CDN加速:静态资源全球分发
- DNS预解析:
<link rel="dns-prefetch">关键域名 - 预加载:
preload关键字体、CSS、JS - 预连接:
preconnect提前建立连接 - 预获取:
prefetch后续页面资源
2.3 请求优化
- 请求合并:减少HTTP请求数量
- 减少Cookie:静态资源使用独立域名
- 持久连接:Keep-Alive减少TCP握手
三、缓存优化(重复利用)
3.1 浏览器缓存
- 强缓存:Cache-Control、Expires(静态资源长期缓存)
- 协商缓存:ETag、Last-Modified(频繁更新文件)
- 内存缓存:memory cache优先级优化
3.2 应用缓存
- Service Worker:PWA技术,离线可用
- LocalStorage/SessionStorage:业务数据缓存
- IndexedDB:大量结构化数据存储
3.3 CDN缓存
- 边缘缓存:就近节点加速
- 缓存策略:不同资源设置不同缓存时间
- 缓存刷新:版本号控制缓存更新
四、渲染优化(提升体验)
4.1 加载体验
- 骨架屏:先展示页面框架,避免白屏
- Loading状态:操作反馈提升用户体验
- 渐进加载:图片、内容逐步展示
4.2 渲染性能
- 减少重排重绘:使用transform/opacity替代top/left
- 硬件加速:will-change、transform3d触发GPU渲染
- 防抖节流:搜索框、滚动、resize事件控制频率
- 虚拟滚动:大数据列表只渲染可视区域
4.3 渲染策略
- 关键CSS内联:避免样式闪烁
- 字体优化:font-display: swap避免文字闪动
- SSR/预渲染:服务端渲染首屏内容(SEO需求)
- 流式渲染:分块传输提升感知速度
五、代码执行优化
5.1 JavaScript优化
- 事件委托:减少事件监听器数量
- 算法优化:降低时间复杂度、空间复杂度
- 内存管理:及时清除引用,避免内存泄漏
- Web Workers:复杂计算移出主线程
5.2 CSS优化
- 选择器优化:避免深层嵌套,减少匹配成本
- 布局优化:flexbox/grid替代float/position
- 动画优化:使用transform/opacity实现动画
5.3 框架优化(Vue/React特定)
- 组件优化:PureComponent、memo、shouldComponentUpdate
- 状态管理:合理划分状态,避免不必要的重新渲染
- 异步更新:nextTick、批量更新减少DOM操作
六、监控与持续优化
6.1 性能监控
- 核心指标:LCP、FID、CLS、FCP、TTI
- 性能分析:Lighthouse、WebPageTest、Chrome DevTools
- 真实用户监控:RUM数据收集分析
6.2 错误监控
- JS错误监控:window.onerror、unhandledrejection
- 资源加载错误:performance.getEntries()监控
- API错误监控:拦截器统一处理
6.3 业务监控
- 用户行为分析:点击流、页面停留时间
- 转化率监控:关键业务流程成功率
- A/B测试:对比不同优化方案效果
七、移动端专项优化
7.1 网络优化
- 弱网优化:请求重试、超时调整、数据压缩
- 离线能力:Service Worker + Cache API
- 数据预加载:WiFi环境下预加载内容
7.2 体验优化
- 手势优化:touch事件防抖、避免误操作
- 动画流畅:60fps动画,减少卡顿
- 内存控制:大图片、长列表内存管理
八、工程化优化
8.1 构建优化
- 增量编译:HMR、缓存提升开发效率
- Tree Shaking:Dead Code Elimination
- 代码分割:动态import、路由级别拆分
8.2 部署优化
- 自动化部署:CI/CD流水线
- 版本管理:合理的版本控制和回滚机制
- 监控告警:性能阈值监控和自动告警
优化优先级建议
高优先级(投入产出比最高)
- 代码分割和懒加载
- 图片优化和CDN
- 缓存策略配置
- 关键资源预加载
中优先级(有明显提升)
- 减少重排重绘
- 防抖节流优化
- 算法和数据结构优化
- 构建配置优化
低优先级(精细化优化)
- 内存泄漏排查
- 复杂动画优化
- 监控体系完善
- A/B测试验证
3. 你是如何实现数据虚拟滚动的?(如何在页面中加载十万条数据)
传统方式的问题:10万条数据 = 10万个DOM节点
会导致:内存爆炸、渲染卡顿、操作不流畅
所以需要使用数据虚拟加载的方式去实现:
1. 创建一个占位元素撑开整个滚动区域,让滚动条范围正确
2. 监听滚动事件,实时计算当前应该显示哪些数据
3. 只渲染可见区域内的几十条数据,其他数据保存在内存中
4. 通过CSS transform将内容定位到正确位置
代码如下:
<template>
<div class="virtual-list" @scroll="handleScroll" ref="scrollContainer">
<!-- 撑开滚动区域的占位元素 -->
<div class="placeholder" :style="{ height: totalHeight + 'px' }"></div>
<!-- 实际渲染的可见项 -->
<div class="visible-items" :style="{ transform: `translateY(${offsetY}px)` }">
<div
v-for="item in visibleItems"
:key="item.id"
class="list-item"
>
{{ item.id }}. {{ item.name }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
allData: [], // 存储所有10万条数据
visibleItems: [], // 当前可视区域内需要渲染的数据
itemHeight: 50, // 每个列表项的高度(固定高度简化计算)
visibleCount: 20, // 可视区域能显示的列表项数量(500px/50px=10,设置20有缓冲)
buffer: 5, // 缓冲区数量,上下多渲染一些项防止滚动空白
scrollTop: 0 // 当前滚动位置
}
},
computed: {
// 计算列表总高度 = 数据总数 * 单项高度
totalHeight() {
return this.allData.length * this.itemHeight
},
// 计算起始索引 = 滚动位置 / 单项高度 - 缓冲区数量
startIndex() {
let start = Math.floor(this.scrollTop / this.itemHeight) - this.buffer
return Math.max(0, start) // 确保不小于0
},
// 计算结束索引 = 起始索引 + 可视数量 + 双倍缓冲区
endIndex() {
let end = this.startIndex + this.visibleCount + this.buffer * 2
return Math.min(end, this.allData.length)
},
// Y轴偏移量 = 起始索引 * 单项高度
offsetY() {
return this.startIndex * this.itemHeight
}
},
methods: {
// 生成10万条测试数据
generateData() {
console.time('生成数据')
this.allData = Array.from({ length: 100000 }, (_, index) => ({
id: index + 1,
name: `用户${index + 1}`
}))
console.timeEnd('生成数据')
},
// 更新可见项
updateVisibleItems() {
this.visibleItems = this.allData.slice(this.startIndex, this.endIndex)
},
// 处理滚动
handleScroll(event) {
this.scrollTop = event.target.scrollTop
this.updateVisibleItems()
}
},
mounted() {
this.generateData()
this.updateVisibleItems()
},
watch: {
// 监听开始和结束索引变化
startIndex() {
this.updateVisibleItems()
},
endIndex() {
this.updateVisibleItems()
}
}
}
</script>
<style scoped>
.virtual-list {
height: 500px;
overflow-y: auto;
border: 1px solid #ccc;
position: relative;
}
.placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.visible-items {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.list-item {
height: 50px;
line-height: 50px;
padding: 0 15px;
border-bottom: 1px solid #eee;
box-sizing: border-box;
}
</style>
-
使用固定高度避免浏览器重排计算
-
设置缓冲区防止快速滚动时出现空白
-
通过transform进行GPU加速渲染
这样无论数据量多大,页面上都只维持固定数量的DOM元素,保证了极致的性能体验。
4. 你是如何实现图片懒加载的?
当页面包含大量图片时,如果一次性加载所有图片,会消耗大量带宽和资源,导致首屏加载缓慢。懒加载技术可以让图片只在需要显示时才加载,显著提升页面性能。
实现思路:
通过 Intersection Observer API 监听图片是否进入可视区域,只有当图片需要显示时才进行加载,从而显著提升页面性能。
数据准备 - 延迟加载 (data-src)
数据分离:把真实图片URL存在data-src属性,初始src为空
// 关键技巧:真实URL存data-src,初始src为空
<img :data-src="img.url" src="" />
这样页面初始化时只加载HTML结构,不发起任何图片请求,极大减少首屏资源加载。
智能监听 - 精准触发
智能监听:用Intersection Observer自动监听图片进入视口
// Intersection Observer 核心逻辑
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src // 触发加载
observer.unobserve(entry.target) // 性能优化
}
})
})
浏览器自动帮我计算图片是否进入视口,进入时才把 data-src 赋值给 src 属性,触发图片下载。
状态管理 - 用户体验
动态加载:进入视口时才赋值src触发加载"
// 加载状态反馈
onLoad(index) { this.images[index].loaded = true }
onError(index) { /* 错误处理 */ }
通过状态管理给用户实时的加载反馈,提升交互体验。
代码如下:
<template>
<div class="lazy-container">
<h2>图片懒加载演示</h2>
<div class="image-list">
<div
v-for="(img, index) in images"
:key="img.id"
class="image-item"
>
<img
ref="lazyImages"
:data-src="img.url"
:alt="img.alt"
class="lazy-img"
@load="onLoad(index)"
@error="onError(index)"
/>
<div v-if="!img.loaded" class="loading">
加载中...
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'SimpleLazyLoad',
data() {
return {
observer: null,
images: [
{ id: 1, url: 'https://picsum.photos/300/200?random=1', alt: '图片1', loaded: false },
{ id: 2, url: 'https://picsum.photos/300/200?random=2', alt: '图片2', loaded: false },
{ id: 3, url: 'https://picsum.photos/300/200?random=3', alt: '图片3', loaded: false },
{ id: 4, url: 'https://picsum.photos/300/200?random=4', alt: '图片4', loaded: false },
{ id: 5, url: 'https://picsum.photos/300/200?random=5', alt: '图片5', loaded: false },
{ id: 6, url: 'https://picsum.photos/300/200?random=6', alt: '图片6', loaded: false },
{ id: 7, url: 'https://picsum.photos/300/200?random=7', alt: '图片7', loaded: false },
{ id: 8, url: 'https://picsum.photos/300/200?random=8', alt: '图片8', loaded: false },
{ id: 9, url: 'https://picsum.photos/300/200?random=9', alt: '图片9', loaded: false },
{ id: 10, url: 'https://picsum.photos/300/200?random=10', alt: '图片10', loaded: false },
]
}
},
mounted() {
// 初始化 IntersectionObserver
// 核心:创建观察器来监听图片是否进入可视区域
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 图片进入可视区域时触发加载
const img = entry.target
img.src = img.dataset.src // 将data-src的值赋给src,触发图片加载
this.observer.unobserve(img) // 加载后停止观察,避免重复触发
}
})
})
// 等待DOM更新完成后,开始观察所有图片
this.$nextTick(() => {
this.$refs.lazyImages.forEach(img => {
this.observer.observe(img) // 为每个图片元素注册观察
})
})
},
methods: {
// 图片加载成功回调
onLoad(index) {
this.images[index].loaded = true // 更新加载状态
},
// 图片加载失败回调
onError(index) {
this.images[index].loaded = true // 标记为已加载(即使是失败状态)
// 可在此处设置错误占位图:img.src = 'error-placeholder.jpg'
}
},
// 组件销毁前清理观察器,避免内存泄漏
beforeUnmount() {
if (this.observer) {
this.observer.disconnect()
}
}
}
</script>
<style scoped>
.lazy-container {
padding: 20px;
max-width: 1000px;
margin: 0 auto;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.image-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.image-item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.lazy-img {
width: 100%;
height: 200px;
object-fit: cover;
background: #f5f5f5;
transition: opacity 0.3s;
}
.lazy-img[src] {
opacity: 1;
}
.lazy-img:not([src]) {
opacity: 0;
}
.loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
color: #666;
}
</style>
Q:为什么不用scroll事件?
A:"scroll事件需要频繁计算元素位置,性能开销大且精度难以保证。Intersection Observer是浏览器原生支持的现代API,性能更好且代码更简洁。"
Q:兼容性如何考虑?
A:"现代浏览器支持良好,对于老浏览器可以通过polyfill降级,或者判断API支持情况后回退到传统方案。"