本文旨在为你提供一份全面的 Web 前端性能优化策略指南,涵盖从用户发起请求到页面完全可交互的整个生命周期。我们将深入探讨各个层面的优化技巧,并提供实用的代码示例和最佳实践。
一.感知性能优化
前端优化第一步,不是层面堆参数,玩指标,而是入让用户感知到页面加载
1.1.lodong
最基础的加载图标
加载图标
1.2.骨架屏(Skeleton Screen)
目的: 在数据还没加载完的时候,用灰色占位结构"骗"用户,提升加载感知速度
原理:
用一张占位图(svg/lottile/gif)模拟页面结构,待真是数据返回后替换掉
实现方案:
1. HTML+css 手写
2. 使用UI框架组件(推荐)
1.Vue+elemnet ui
<el-skeleton rows="3" animated />
2.React + Ant Design:
<Skeleton active paragraph={{ rows: 3 }} />
1.3.渐进式图片加载
不一次性记载高清大图,而是先加载模糊缩略图,然后再替换成清晰图
用户看到页面有图,即便不清晰,也会觉得"页面已经加载出来了"
1.4.渐进式渲染:
尽快显示页面基本结构和内容,然后逐步加载增强功能或次要内容,与骨架屏/占位符结合
二 HTML优化: 从源头提升性能与可维护性
2.1 压缩HTML 文件: 体积小,加载快
上线发布前,一定要对HTML文件进行压缩,去除空格,换行,注释等无意义内容,这样不仅可以显著减少页面体积,还能:
- 加快页面加载速度
- 降低用户流量消耗
- 提升整体访问体验
注意:
1. Vue2+Webpack 项目中HTML 不会自动压缩
虽然Webpack在生产模式下会自动压缩js和css,但默认不会压缩HTML文件,因此:
- 如果你使用的是Vue2项目(基于Webpack构建)
- 那么你需要手动配置html-webpack-plugin 插件的minify选项
- 否则最终构建产物中的index.html 仍然可能包含大量空格,换行和注释内容
2. Vue3+Vite 项目的默认行为(Vite会自动压缩HTML,但想进一步控制也可以使用插件)
2.2. 删除无用注释和冗余属性
- 删除开发时遗留的冗余属性
注释只在开发过程中有用,发布上线后应全部移除,除非有必要保留特定说明(如版权信息)- 移除冗余的type属性
在HTML5中,以下属性都是默认值,不需要写出来<!-- 不推荐 -->
<script type="text/javascript" src="main.js"></script>
<link rel="stylesheet" type="text/css" href="main.css"><!-- 推荐 -->
<script src="main.js"></script>
<link rel="stylesheet" href="main.css">
2.3 使用语义化标签
语义化标签能清晰表达页面结构,不但对开发者友好,也对搜索引擎更友好
标签 | 说明 |
---|---|
<header> |
页面头部 |
<nav> |
导航区域 |
<main> |
主体内容 |
<section> |
内容区块 |
<article> |
独立文章段落 |
<footer> |
页面底部 |
好处:
- 可读性强,维护更方便
- SEO更友好,关键词权重更清晰
- 屏幕阅读器可理解机构,增强无障碍性
2.4 精简DOM 结构,减少层级嵌套
浏览器在渲染页面时,要先解析DOM树,DOM层级越深,结构越复杂,性能就越差
优化建议:
- 使用最小必要的标签结构
- 减少不必要的div嵌套
- 避免深层结构影响渲染效率
2.5 减少iframne使用
<iframe>可以嵌入其他网页,但它有很多问题:
- 无法被搜索引擎抓取内容
- 会增加额外的资源加载
- 安全性低,调试困难
建议:
除非必须(如嵌入第三方支付,地图等),尽量避免iframe
2.6 减少 HTTP 请求次数
HTML 页面中引用了大量的外部资源(CSS/JS/图片),每个都需要一次HTTP请求
优化方式:
- 合并多个CSS/JS 文件
- 使用CSS Sprites合并小图标 (把多个小图标(如网站中的按钮、图标等)合并成一张大图,然后通过 CSS 的
background-position
属性来显示其中的某一个图标。) - 使用HTTP/2 或HTTP
2.7 减少重排(Reflow)与重绘(Repaint)
重排: 当渲染树种的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,这就成为回流(reflow),每个页面至少需要一次回流, 就是在页面第一次加载的时候
哪些操作可以影响重排:
- 添加/删除元素
- display: none
- 移动元素位置
- 操作styles
- offsetLeft,scrollTop,clientWidth
- 修改浏览器大小,字体大小
重绘: 重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性绘制,使元素呈现新的外观,重绘发生在元素的可见的外观被改变,但并没有影响到布局的时候
重点: 重排必定会引发重绘,但重绘不一定会引发重排
三. CSS优化
3.1 尽量少用@import
1. 使用@import 引入会影响浏览器的并行下载,使用@import 引用的css文件只有在引用它的那个css文件被下载,解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析,构建render tree等一系列操作,这就导致浏览器无法并行下载所需的样式文件
2. 多个@import 会导致下载顺序紊乱,在IE中,@import 会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件优先于@import下载,并且打乱甚至破坏@import 自身的下载
3.2. 避免!import ,可以选择其他选择器
因为这破坏了样式表的固有的级联规则,使调试bug变得更加困难,它很容易使用不当,而且容易成倍增加,尤其是在滥用时,您可以轻松的!import得出一个带有要覆盖的规则的元素,这时您通常不得不重构样式,或使用另一个!import规则来加剧问题
如果你一定要使用,最好定义基本样式尽可能靠近html或body元素,并且要覆盖时,请尽量避免使用特殊性,这样,您就有足够的空间进行更改,
3.3 不要在ID选择器前面进行嵌套其它选择器
主要有两个原因:
1. ID 选择器本来就是唯一的而且权值又大,前面嵌套,完全是浪费性能
//浪费性能
.content #dom{ }
2. 除了嵌套,在ID选择器前面也不需要加标签或者其它选择器,比如div#dom 或者.box#dom.这两种方式完全是多余的,理由就是ID在页面就是唯一的,前面加任何东西都是多余的!
div#dom{ }
3.4 使用 CSS 压缩工具减少文件体积
使用构建工具自动压缩CSS内容,去除:
- 空格/注释
- 多余的分号,换行
- 无用的样式声明
常用工具:
- PostCSS+cssnano
- Vite,Webpack 默认已集成压缩功能
3.5 避免使用复杂或低效的选择器
CSS的选择器写法会影响样式匹配的效率,匹配从右向左进行,因此应该:
- 避免过于深的嵌套
- 避免通配符选择器 *
- 优先使用类选择器(.box)而不是标签,ID 或属性选择器
3.6 使用简写属性
/* 不推荐 */
margin-top: 10px;
margin-right: 10px;
margin-bottom: 10px;
margin-left: 10px;/* 推荐 */
margin: 10px;
3.7 样式内联 vs 外链:合理选择
类型 | 优点 | 缺点 |
---|---|---|
内联样式 <style> |
加载更快,适合首屏 | 不可复用,文件混乱 |
外链 CSS | 结构清晰,可缓存 | 首次渲染需等待 |
建议:
- 首屏样式可内联
- 其他部分使用外链并开启浏览器缓存
四. js优化
4.1 脚本加载优化(控制执行时机)
使用defer和async
- defer: 等待HTML解析完再执行,按顺序执行,推荐用于大部分脚本
- async: 加载完立即执行,不保证顺序,适合统计类脚本
<script src="main.js" defer></script>
<script src="analytics.js" async></script>
属性 | 加载方式 | 执行时机 | 顺序保证 |
---|---|---|---|
无 | 阻塞渲染 | 加载完成立刻执行 | 是 |
defer | 异步加载 | DOM 解析完成后执行 | 是 |
async | 异步加载 | 下载完成立即执行 | 否 |
4.2 按需加载(拆分模块)
大型网站或应用不要一次性加载所有JS,应按需加载,提升首屏速度
常见方式:
- 懒加载(用户滚到某区域才加载js)
- 路由级代码分割(前端框架如Vue/React都支持)
- 使用import( )动态加载
示例(ES模块懒加载):
// 示例(ES模块懒加载):
button.addEventListener('click', async () => {
const module = await import('./some-module.js');
module.doSomething();
});
4.3 节流和防抖(高频事件优化)
1.防抖 debounce: 一段时间内只执行最后一次函数调用(适合输入框搜索,按钮点击)
原理: 每次事件触发,都会清除上一次的定时器,然后重新计时
/* fn: 要被防抖的函数 delay: 延迟时间(默认300毫秒) */ function debounce(fn,delay=300){ let timer=null; //返回一个新函数,这个函数才是真正绑定事件用的函数 return function(...args){ clrearTimeout(timer); //清除上一个定时器 //延迟后执行 timer=setTimerout(()=>{ //执行目标函数fn, this保持原来的调用上下文,args把刚才传入的新函数的参数传进去 fn.apply(this,args); },delay); } } //用法 function handleSearch(keyword) { console.log('🔍 正在搜索:', keyword); } const debouncedSearch = debounce(handleSearch, 500); const input = document.getElementById('searchInput'); input.addEventListener('input', function (e) { // 每次输入触发,都传入当前输入框的内容 debouncedSearch(e.target.value); });
2. 节流 throttle: 每隔一段固定时间执行一次函数调用(页面滚动,窗口缩放等)
频繁触发事件时,限定函数没X毫秒只能执行一次
原理: 设定一个时间阈值,每次触发只能呢过在间隔超过这个时间时才执行
function throttle(fn,delay=300){
let lastTime=0; //上一次执行的时间
return function(..args){
const now=Date.now(); //获取当前时间戳(单位毫秒)
//判断距离上次执行是否超过了delay时间
if(now-lastTime>delay){
lastTime=now; //更新上一次执行时间为当前时间
fn.apply(this,args); //调用原函数,保持this上下文和参数传递
}
}
}
function handleClick(event) {
console.log('按钮被点击,时间:', new Date().toLocaleTimeString());
}
// 创建节流后的函数,限制300ms内只能执行一次
const throttledClick = throttle(handleClick, 300);
// 绑定事件
const btn = document.getElementById('btn');
btn.addEventListener('click', throttledClick);
4.4. 使用事件代理减少事件监听数量
不要给每个元素绑定事件,使用事件委托可极大减少内存消耗
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log('你点击了', e.target.textContent);
}
});
4.5. 避免内存泄露(及时清理)
常见泄露场景:
定时器未清除
事件监听未解除
闭包保留无用对象
DOM 被移除但变量还在引用
4.6 合理的ajax请求
关于回来内容相同的请求,没必要每次都直接从服务端拉取,合理运用AJAX呼应速度并减轻服务器压力
4.7 长列表虚拟滚动优化
虚拟列表是一种用来优化长列表技术的 ,它可以保证在列表元素不断增加,或者列表元素很多的情况下,依然拥有很好的滚动,浏览性能,它的核心思想在于: 只渲染可见区域附近的列表元素,下图左边就是虚拟列表的效果,可以看到只有视口内和临近视口的上下区域内的元素会被渲染
推荐几个基于框架的开源实现:
- 基于React的
react-virtualized
- 基于Vue 的
vue-virtual-scroll-list
- 基于Angular的
ngx-virtual-scroller
五. 图片优化
5.1.根据实际需要选择色深,压缩
减小图片的大小可以减少页面加载时间,可以使用图片压缩工具来压缩图片,以减小文件大小
5.2 图片懒加载
一般来说,我们访问网站页面时,其实很多图片并不在首屏中,如果我们都加载的话,相当于是加载了用户不一定看到图片,这显然是一种浪费,解决的核心思路就是懒加载: 实现方式就是先不给图片设置路径,当图片出现在浏览器可视区域时才设置真正的图片路径
5.3 采用svg图片或者字体图标
因为字体图标或者SVG是矢量图,代码编写出来的,放大不会失真,而且渲染速度快,字体图标使用时就跟字体一样,可以设置属性,例如font-size,color等等, 还有一个优点是生成的文件特别小
5.4 Base64
将图片的内容以Base64格式内嵌到HTML中,可以减少HTTP请求数量,但是,由于Base64编码用8位字符表示信息中的6个位,所以编码后大小约比原始值扩大了33%
六. 网络与缓存优化
6.1 启用浏览器缓存(强缓存与协商缓存)
浏览器缓存可以让用户在访问同一页面时服用之前下载的资源,大幅减少加载时间与网络请求
1. 强缓存(Expires/Cache-Control)
- Cache-Control: max-age=31536000
表示资源可以在本地缓存一年,无需再次请求服务器 - Expires: Tue,20 Jun 2025 08:00:00 GMT
设置过期时间,达到时间前不会重新请求
2.协商缓存(Last-Modified / ETag )
- 当资源更新,服务器返回新的响应,否则返回304 Not Modified
- Last-Modified: 对比上次修改时间
- Etag: 资源指纹,对比是否变更
6.2 使用Gzip/Brotil 压缩传输内容
压缩HTML,CSS,JS等文本资源可大幅减小文件体积(可达70%),加快加载速度
6.3 减少请求次数(合并资源)
将多个CSS/JS 文件合并成一个文件,减少HTTP请求数,尤其对HTTP/1.1非常重要
推荐做法:
- 打包工具配置合并(Webpack,Vite)
- 使用按需加载(Tree-shaking)只加载用到的模块
6.4 使用HTTP/2 多路复用
相比HTTP/1.1, HTTP/2 支持:
- 多个请求共用一个连接
- 头部压缩
- 请求优先级和服务器推送
可以避免"队头阻塞"问题,显著提升资源并发加载效率
建议:
- 启用HTTPS后基本都会开启HTTP/2
- 云服务器或CDN一般默认支持
6.5 使用CDN静态资源加速
将JS.CSS.图片等静态资源部署到全球节点分布的CDN上,可以:
- 减少服务器压力
- 加快资源下载速度
- 避免跨域访问问题(如使用公共库CDN)
6.6 开启DNS预解析(DNS Prefetch)
DNS解析时间虽短,但在用户体验中也会产生延迟
可以通过域解析域名,提前建立连接:
<link rel="dns-prefetch" href="//cdn.exampe.com">
6.7 减少重定向次数
重定向(如hhp->https.或example.com--> www.example.com)会增加一次请求延迟
建议:
- 使用服务器自动跳转或永久301重定向,尽量避免多次跳转
- 页面内统一使用最终地址
6.8 合理使用预加载和预请求
1. 预加载preload: 告诉浏览器" 马上就需要这个资源", 优先下载
<link rel='preload' href="/main.js" as='script' />
2. 预请求prefetch:告诉浏览器" 未来可能用到",利用空闲宽带加载
<link rel='perfetch' href="/next-page.js"/>