前言 🎉
本文将从三个角度 (页面渲染,打包优化,总体优化
)分析前端性能优化的一些方案。如有错误,欢迎指出,也可以在评论区提出你对性能优化的一些见解,相互学习,共同进步!💪
一、页面渲染 🌈
1. 减少页面重绘(repaint)和回流(reflow)
回流:计算可见节点的位置和几何信息;
重绘:重新绘制节点,将渲染树的每个节点都转换为屏幕上的实际像素。
-
🍀 减少使用 css 属性简写,如:用border-width, border-style, border-color代替border。因为css简写把所有值初始化为
initial
,尽量不使用属性简写可以最小化重绘(repaint)和回流(reflow),(实际工作中,由于css简写带来的性能影响微乎其微,再加上css全都展开写还会增加code size,另外css简写还能解决一些样式覆盖的问题,写起来也比较简单快捷,所以实际工作个人怎么顺手怎么来就好~) -
🍀 在 GPU 上渲染动画,浏览器针对处理 CSS 动画进行了优化,可以很好地处理触发重排(因此也导致重绘)的动画属性。为了提高性能,可以将设置了动画的节点从主线程移到 GPU 上。 可以触发 GPU 硬件加速的css属性有:
transform、filter、will-change、position: fixed
等,只要元素被提升为图层,动画就会在 GPU 中完成,从而改善性能,尤其是在移动设备上(但是不能滥用,会导致性能问题)。 -
🍀 使用css属性
will-change
提高性能,它的作用是告诉浏览器元素的哪些属性需要修改,使浏览器能够在元素实际更改之前设置优化,通过在实际更改前执行耗时的工作以提升性能(will-change
同样不能滥用,一半在有很大计算量的动画出现性能问题时考虑用 will-change 提高性能)。 -
🍀 通过修改className批量修改元素样式;
-
🍀 复杂的动画元素定位要设置为
fixed
或absoult
,避免引起回流; -
🍀 不使用table布局(table元素一旦触发回流就会导致table里所有的其它元素回流);
-
🍀 DOM 元素上下移动用用translate替代top修改;
-
🍀 需要创建多个DOM节点时,使用DocumentFragment一次性创建。
-
🍀 元素适当地定义高度或最小高度,否则元素的动态内容载入时,会出现页面元素的晃动或位置,造成回流(比如图片要定义宽高,避免页面塌陷,同时减少回流);
-
🍀 减少使用层级较深的选择器,或其他一些复杂的选择器,以提高CSS渲染效率;
-
🍀 在大量修改元素样式时,可以先用
display: none
将其隐藏,修改完再设置为display: block
,这样只会造成两次回流; -
🍀 必要时使用css属性
contain
将一个元素及其内容与文档流隔离,防止元素内部在其包围盒外产生副作用。
2. 图片压缩,图片分割,精灵图
-
🍀 图片压缩:开发中比较重要的一个环节,现在很多图床工具都自带压缩功能的。
-
🍀 图片分割:如果页面需要加载一张效果图,比如真机渲染图,UI设计师不允许压缩的时候,我们就可以将图片分割,然后再用css布局将图片拼接到一起。
-
🍀 精灵图 :与图片分割相反,精灵图是将很多小图片合并到一张大图里,这样加载页面的时候,只需要加载一张图片,就可以加载出来所有的页面元素。这样在一定程度上提高了页面加载速度。而我们使用精灵图的方式,也是通过定位,即通过background-position来移动背景图,从而显示出我们想要显示出来的部分。
但是精灵兔有个很大的不足,那就是牵一发而动全身,因为我们使用精灵图需要准确测量每个元素的位置,一旦要调整页面,将会是一项很麻烦的工作。
3. 字体包压缩
-
🍀 问题描述: 在做类似活动h5的时候,难免需要使用
@font-face
接口引入一些字体包,以达到丰富的页面效果。但是完整的字体包文件一般都很好几兆,加载页面时,不仅会阻塞页面渲染,还会导致文字开始不会显示,直至字体包加载完才会显示文字。 -
🍀 解决方式: 可以使用
font-spider
字蛛将要使用到的文字提取出来。
4. 懒加载/预加载资源
-
🍀 懒加载: 简言之就是只有当图片出现在浏览器的可视区域内时,才加载图片让图片显示出来(在此之前可以将所有图片元素的路径全都统一设置成一张1*1px的占位图)。
判断图片出现在浏览器可视区域的方法:图片距离顶部的高度(offsetTop) - 页面被卷去的高度(scrollTop) 〈= 浏览器的可视区域的高度(innerHeight)
-
🍀 预加载: Resource Hints(资源预加载)包括预连接、资源与获取、资源预渲染等。
预加载的思路
有如下两个:当前将要获取资源的列表;
通过当前页面或应用的状态、用户历史行为或 session 预测用户行为及必需的资源.
实现Resource Hints的方法 有很多种,可分为基于link
标签的DNS-prefetch、subresource、preload、 prefetch、preconnect、prerender
,和本地存储localStorage
。
二、打包优化 🌞
1. webpack优化resolve.alias配置(vite同理)
resolve.alias
配置通过别名来将原导入路径映射成一个新的导入路径。
可以起到两个作用:1.起别名;2.减少查找过程。
例如:
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
2. webpack优化resolve.extensions配置(vite同理)
resolve.extensions
代表后缀尝试列表,它也会影响构建的性能,默认是:
extensions: ['.js', '.json']
例如遇到require('./data')这样的导入语句时,Webpack会先去寻找./data.js文件,如果该文件不存在就去寻找./data.json文件,如果还是找不到就报错。
【所以后缀尝试列表要尽可能的小,不要把项目中不可能存在的情况写到后缀尝试列表中,频率出现最高的文件后缀要优先放在最前面,以做到尽快的退出寻找过程。】
在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。例如在你确定的情况下把require('./data')写成require('./data.json')。
resolve: {
extensions: ['.js', '.vue', '.json'],
}
3. webpack缩小loader范围
loader是很消耗性能的一个点,我们在配置loader的时候,可以使用include和except来缩小loader执行范围,从而优化性能。 例如:
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
include: [resolve('src/icons')]
},
4. split chunks代码分包
在没配置任何东西的情况下,webpack 4 就智能的帮你做了代码分包。入口文件依赖的文件都被打包进了main.js,那些大于 30kb 的第三方包,如:echarts、xlsx、dropzone等都被单独打包成了一个个独立 bundle。 其它被我们设置了异步加载的页面或者组件变成了一个个chunk,也就是被打包成独立的bundle。 它内置的代码分割策略是这样的:
新的 chunk 是否被共享或者是来自 node_modules 的模块;
新的 chunk 体积在压缩之前是否大于 30kb;
按需加载 chunk 的并发请求数量小于等于 5 个;
页面初始加载时的并发请求数量小于等于 3 个;
大家可以根据自己的项目环境来更改配置。webpack配置代码如下:
splitChunks({
cacheGroups: {
vendors: {
name: `chunk-vendors`,
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial',
},
dll: {
name: `chunk-dll`,
test: /[\\/]bizcharts|[\\/]\@antv[\\/]data-set/,
priority: 15,
chunks: 'all',
reuseExistingChunk: true
},
common: {
name: `chunk-common`,
minChunks: 2,
priority: -20,
chunks: 'all',
reuseExistingChunk: true
},
}
})
没有使用webpack4.x版本的项目,可以通过按需加载的形式进行分包:
vite配置代码如下:
build: {
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称
entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称
assetFileNames: '[ext]/[name]-[hash].[ext]' // 资源文件像 字体,图片等
}
}
}
5. tree shaking
tree shaking
(摇树)。望文生义,它是用来清除我们项目中的一些无用代码,它依赖于ES中的模块语法得以实现。 比如日常使用lodash的时候:
import _ from 'lodash'
如果如上引用lodash库,在构建包的时候是会把整个lodash包打入到我们的bundle包中的。
import _isEmpty from 'lodash/isEmpty';
如果如上引用lodash库,在构建包的时候只会把isEmpty这个方法抽离出来再打入到我们的bundle包中。
tree shaking
可以大大减少包体积,是性能优化中的重要一环。 在 vite 和 webpack4.x 中都已经默认开启tree-shaking。
6. vite关闭一些打包配置项()
webpack也有类似的配置,自行查阅
build: {
terserOptions: {
compress: {
//生产环境时移除console
drop_console: true,
drop_debugger: true,
},
},
//关闭文件计算
reportCompressedSize: false,
//关闭生成map文件 可以达到缩小打包体积
sourcemap: false, // 这个生产环境一定要关闭,不然打包的产物会很大
}
三、总体优化 🌻
1. SSR服务端渲染
SSR
(Server Side Rendering),即服务端渲染。它指的是渲染过程在服务端完成,最终的渲染结果 HTML 页面通过 HTTP 协议发送给客户端,又叫"同构"。
SSR主要带来的好处就是 SEO 和首屏加载速度大大提高。
目前流行的 Vue/React 前端框架,都已经推出了SSR的解决方案:
2. 开启 gzip 压缩
Gzip
对文件进行压缩,能大大提高首屏加载速度,对于纯文本文件我们可以至少压缩到原大小的40%。注意:图片最好不要进行gzip压缩!
为了减轻服务器压力,我们可以在打包时就生成gzip压缩文件,具体操作如下:
第一步,在vue项目中安装依赖并将productionGzip改为true,开启Gzip压缩: npm install --save-dev compression-webpack-plugin
第二步,运行 npm run build打包项目,这时可能会报错,提示ValidationError: Compression Plugin Invalid Options。根据官网提示,需要将CompressionWebpackPlugin的设置由asset改为filename。
第三步,npm run build打包项目,生成.gz文件即为成功
3. Brotli 算法压缩
Brotli 压缩算法
是 Google 2015年推出的无损压缩算法。
- 启用 Brotli 压缩算法,对比 Gzip 压缩 CDN 流量再减少 20%。
- 针对常见的 Web 资源内容,Brotli 的性能相比 Gzip 提高了 17-25%;
- 当 Brotli 压缩级别为 1 时,压缩率比 Gzip 压缩等级为 9(最高)时还要高;
- 在处理不同 HTML 文档时,Brotli 依然能够提供非常高的压缩率。
- 兼容性:目前除了 IE 和 Opera Mini 之外,几乎所有的主流浏览器都已支持 Brotli 算法。
使用条件 >>>
-
Content-Length 大于 256 字节;
-
客户端请求头需携带 Accept-Encoding: br;
-
Content-Type 满足如下 MIME 列表:
text/plain
text/javascript
text/css
text/xml
text/x-component
application/javascript
application/x-javascript
application/xml
application/json
application/xhtml+xml
application/rss+xml
application/atom+xml
application/x-font-ttf
application/vnd.ms-fontobject
image/svg+xml
image/x-icon
font/opentype
text/html -- default
vite项目开启 brotli
压缩 >>>
-
使用 vite-plugin-compression (opens new window)对平台进行
gzip
或者brotli
压缩,nginx
对这两种压缩模式都支持,压缩后部署到nginx
将极大提高网页加载速度。 -
修改
.env.production
文件,设置VITE_COMPRESSION
全局变量即可,如下:不开启压缩,默认
VITE_COMPRESSION = "none"
//以下配置压缩时不删除原始文件的配置
开启 gzip 压缩,
VITE_COMPRESSION = "gzip"开启 brotli 压缩
VITE_COMPRESSION = "brotli"同时开启 gzip 与 brotli 压缩
VITE_COMPRESSION = "both"
//以下配置压缩时删除原始文件的配置
开启 gzip 压缩
VITE_COMPRESSION = "gzip-clear"开启 brotli 压缩
VITE_COMPRESSION = "brotli-clear"同时开启 gzip 与 brotli 压缩
VITE_COMPRESSION = "both-clear"
4. 缓存
缓存的原理就是更快读写的存储介质+减少IO+减少CPU计算=性能优化。而性能优化的第一定律就是:优先考虑使用缓存。
缓存的主要手段有:浏览器缓存、CDN、反向代理、本地缓存、分布式缓存、数据库缓存。
5. Ajax可缓存
Ajax在发送的数据成功后,为了提高页面的响应速度和用户体验,会把请求的URL和返回的响应结果保存在缓存内,当下一次调用Ajax发送相同的请求(URL和参数完全相同)时,它就会直接从缓存中拿数据。 在进行Ajax请求的时候,可以选择尽量使用get方法,这样可以使用客户端的缓存,提高请求速度。
6. 组件按需引入
使用第三方组件库时,要按需引入,例如import { Button } from 'vant';
7. 动态加载
使用import()
动态引入的语法引入一些第三方库和组件,例如我们在测试环境下开启 vconsole
调试:
if (location.host !== '正式环境域名') {
import('@/utils/vconsole')
}
8. 组件异步加载
组件异步加载有两种方式:
// 1. import懒加载
() => import('@/pages/xxx.vue')
// 2. 使用require
resolve => require(['@/pages/xxx.vue'], resolve),
9. 路由懒加载
路由懒加载是组件异步加载的运用:
//routes
{
path: '/index',
name: 'index',
component: () => import('@view/xxx.vue'),
//或者 component: require(['@/view/xxx.vue'], resolve),
meta: { title: '首页' }
}
10. CDN内容分发
CDN的全称是Content Delivery Network,即内容分发网络。静态文件,音频,视频,js资源,图片等都可以放CDN上。
CDN的原理类似于京东,其实就是将资源放在遍布世界各地的服务器上,访问资源的时候,请求会重定向到 离用户最近的服务节点上。
11. 域名发散(域名收敛)
就是将同一站点下的静态资源分布在不同域名下。例如:
- DNS预解析
当你的网站第一次请求某个跨域域名时,需要先解析该域名(例如页面访问cdn资源,第一次访问需要先解析cdn)。可以在请求的Timing上看到有一个DNS Lookup阶段,而在这个请求之后的其他该域名的请求都没有这项时间支出。
<link rel="dns-prefetch" href="xxx.com">
使用说明: >>>
-
http页面下所有的a标签的href都会自动去启用DNS Prefetch,也就是说,你网页的a标签href带的域名,是不需要在head里面加上link手动设置的。https页面需要使用meta标签强制开启:
<meta http-equiv="x-dns-prefetch-control" content="on"> -
dns-prefetch适用于网页引用了大量其他域名的资源,例如淘宝。
13. web worker
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
合理实用web worker可以优化复杂计算任务。