前言
性能优化总是被面试官问,每次都只能答出那么几个,其实大家都清楚性能优化手段非常多,只要是能让用户体验更好的手段都可以称之为性能优化,本期我就总结下我所知道的性能优化手段,欢迎各位补充~
一、减少 http 请求
比如一个项目中,登录之后需要展示登录时的昵称,这个昵称就不需要重新发一次接口请求去拿到了,可以通过路由传参拿到
为何说减少 http
请求是一种性能优化?因为一个 http
的请求中间的过程非常多,常见的 输入url后到页面展示的过程 大家都清楚,步骤很多
这个过程还不清楚的,可以跳转这两篇文章,都写得很详细
二、使用 http2.0
当然,现在基本上都是用 http2.0
这个版本的 http
了,那为什么使用 http2.0
会性能更优?
http2.0
更优是相对于此前的版本,此前 1.1
版本因为有多个 keep-alive
长连接导致了 http
的队头阻塞问题,同时多个长连接也带来了 带宽 用不满的问题
2.0
针对 1.1
的这些问题,在一个域名下,多个 tcp
长连接合并成了一个,这就是多路复用,并且 2.0
采用了二进制分帧层,将每个请求分成了一帧一帧的数据进行传输并打上标记,可以给特定的数据帧加急处理
同样,这个问题我在文章 聊聊http发展史 中讲得非常详细了
三、使用 SSR 服务端渲染
SSR
可以让首屏加载更快,带来更好的 SEO
前端基本上现在都是 SPA
单页应用,单页应用的缺陷就是首屏加载很慢。使用 SSR
服务端渲染可以带来更好的 SEO
,SEO
就是搜索引擎优化,搜索引擎就是爬虫,可以更好的爬数据
感觉掘金在
seo
上比不过csdn
,每次搜文章,csdn
永远在前面😭
其实早期的前后端不分离开发方式就是服务端渲染,就是 jsp
,后端直接向前端返回一个 html
文件,既然如此为何如今又要搞一个分离式开发方式,这是为了开发效率,开发效率的优点受益比服务端渲染高,不分离开发方式前端工作量太少了,效率很低
下面可以看下 vue
是如何做 ssr
的
vue - ssr
其实 vue
的 ssr
在官网上就有
我们可以在后端新建一个文件运行这段代码试试
1 | javascript复制代码// 此文件运行在 Node.js 服务器上 |
打印输出:<button>1</button>
createSSRApp
可以帮我们创建一个组件,这样我们就可以不需要 vue
文件创建 vue
组件了,这个组件里面可以创建数据源,模板等,然后借助 renderToString
帮我们把模板当成字符串渲染成 html
接下来借助 express
来实现一个后端 demo
express
相比koa
,express
的路由无需另外安装,koa
其实是基于express
打造的
1 | xml复制代码import express from 'express' |
server.get
就是路由,里面的逻辑是创建一个 vue
组件,然后渲染成字符串,返回给浏览器,这就是 jsp
, jsp
就是 ssr
,现在就可以访问 localhost:3000
看到一个 html
页面了,里面是一个按钮
这个按钮目前点击是不会生效的,因为浏览器端没有 vue
的请求,还需要将 app
挂载 amount
到 #app
上,我们还需要在项目根目录下新建一个 app.js
,如下,目的是在服务端和客户端之间共享
1 | javascript复制代码// app.js (在服务器和客户端之间共享) |
根目录下新建一个 client.js
1 | javascript复制代码// client.js |
这么做就是把 server.js
中定义组件的代码搬出去写了
在 server.js
的 html
中引入 vue
源码,再引入 client.js
,再引入路径
1 | xml复制代码import express from 'express' |
就跟着 vue
官方文档一步一步就实现了一个 ssr
的 demo ,现在访问 localhost:3000
点击按钮可以实现累加的效果,也就是是实现了响应式
然后你可以去查看页面源代码, button
可以看到,若是 vue
的项目,你是看不到任何标签的,这样搜索引擎这样的爬虫就可以爬到你的数据,假设这是个买衣服的网站,你就可以尽可能地把信息展现到 html
中来
像是这样的实现,其实我们就可以把这个按钮组件写成一个登录组件,然后点击后跳转到另一个页面,那个页面又可以写其他端口,这样就是解决了首屏加载过慢的问题
公司做的产品只要是给用户用得,基本上都会去做
ssr
四、合理使用 cdn
CDN(Content Delivery Network)全称为内容分发网络
像是 cdn
就涉及到服务器分布的问题了,谷歌的服务器放在深海中的,为了方便散热🤣。像是我们访问国外的网站,就算有魔法加持也会比较慢,就是因为一个网络请求跨越的距离太远了。像是国内,假设阿里的服务器在杭州,西藏的朋友访问淘宝就会有点慢,因此,这些大厂基本上都会再设置云服务器方便偏远地区访问。
合理使用
cdn
,为何要说合理使用,就是因为服务器资源太贵了,不可能每个城市都给你搞一个云服务器
合理使用 cdn
是运维部门该干的事情,和前后端无关
当然不排除某些公司或者小厂,为了降本增效,运维工作同时交给了后端来干。另外前后端会有一个测试服务器,可以随便测自己的项目。
五、将 css 放在文件头部,将 js 放在文件底部
css
放在 body
当中其实也可以,这么带来的效果确实 html
优先出现在用户面前,但是没有样式,降低用户体验!因此 css
放到文件头部非常重要
有时候页面炸了的时候,没有样式,全是文字和🔗,非常影响用户体验,这种效果还不如给用户看白屏
当然 js
也可以写在上面,得看情况,若 js
在页面加载中需要用到,就需要放到前面,这种情况很少。 js
引擎线程和浏览器渲染线程是互斥的,因为 js
也可以操作 dom
结构,可能会冲突
1 | xml复制代码<!DOCTYPE html> |
当然,你也可以用 async
和 defer
解决 js
阻塞问题
async & defer
二者的共同点均是异步加载
js
js
是有两个步骤的,先是加载
,后是执行
defer
是延迟的意思,就是让 js
延迟执行,等 html
加载完毕再去执行,因此 defer
的效果就是把 js
放到文件底部
async
不会延迟执行 js
,它的效果是 js
照常加载,加载完毕就需要立即执行 js
,期间 html
就不能执行了
因此 defer
看样子更好说话, js
加载完毕等 html
执行完毕再去执行 js
,而 async
就难说话一点,加载完 js
后立即执行 js
。 defer
带来的效果其实就是将 js
放到文件底部, async
的效果就是放在文件头部
六、使用精灵图(雪碧图)
精灵图(雪碧图)就是一种将多个小图标或图像组合到单个图像文件中的技术
有个很经典的🌰就是豆瓣官网右上角一排的文字图片
既然是一张图那如何实现可以点到每个 li
的呢
这张图并不是直接完整的放到页面中去,否则无法实现各点各的,这些个 li
其实都用到了这张图,只是每个 li
都会单独去调整这张图的位置,比如读书用到了这张图,只把读书二字展现了出来
这么做的意义就是减少 http
的请求次数,若这 8 个栏目都用自己单独的图片,就意味着要发 8 个 http
请求,用雪碧图就一次请求多处使用
ui
设计师若是不懂这个优化,我们前端可以跟她提这个需求,做成一张图
七、善用 http 缓存:强缓存 & 协商缓存
关于这两个缓存,此前专门出过一期文章详细讲解过:深入浅出【强缓存 & 协商缓存】,下面这段话不太理解可以详细看下这篇
二者都是后端控制的东西,强缓存是响应头添加 'Cache-Control': 'max-age=xxx'
字段, max-age
是过期时间,强缓存后无法缓存输入 url
后的 get
请求,想要缓存这个请求需要靠协商缓存来实现,协商缓存的实现是在强缓存的基础上添加一个 'Last-Modified': stats.mtimeMs
或者 etag
字段,若检查到前端返回的 If-Modified-Since
时间一致,后端就返回 304
状态码给前端,浏览器就从缓存中读取静态资源
八、压缩文件
压缩文件前后端都可以做,前端压缩就是打包,将代码打包成密密麻麻的样子,剔除掉无用的换行,空格等,这样文件大小就可以变小,文件变小下载速度就会快
后端的压缩就是向前端返回静态资源进行压缩,比如图片转格式等等
九、懒加载
懒加载的核心思想就是图片不在可视区范围中不加载,滚到可视区范围内才给予加载。这么做就是没必要页面初次加载所有图片
懒加载的实现早期就出过一篇文章:手把手教你实现js懒加载
懒加载的实现需要获取到可视区范围的高度,以及每张图片的高度,监听用户滚动的过程中图片是否进入范围内,进入时才赋值 src
, src
只要有值就一定会发送 http
请求,此前存放 src
的属性可以任意取名,当然一般我们取名为 data-
前缀,比如下面这样
<img src="" data-src="****">
IntersectionObserver
IntersectionObserver
这个 api
也可以用于实现懒加载,这个方法可以用来监听目标元素和祖先元素是否相交,交叉多少都可以实现
1 | ini复制代码<!DOCTYPE html> |
当观察到一个图片元素进入视口时,触发 IntersectionObserver
的回调函数。在回调函数中,首先检查触发事件的元素是否进入了视口entry.isIntersecting
,如果是,则将该元素的 src
属性设置为 data-original
属性的值,即加载图片。然后,移除 data-original
属性,以确保图片只加载一次。最后,调用 io.unobserve(entry.target)
停止对该元素的观察,避免不必要的性能消耗。
十、尽量用 css ,字体来代表图片
有些背景图就是一个渐变色,这个时候就不需要放图片了, css
能够实现就用 css
反正能用 css
就用 css
,能不用图片就不用图片。当然对于程序员来讲,谁不想早点下班呢,用 css
就意味着自己多点工作,一张图片多简单
十一、使用 webp 格式的图片
webp
格式的图片是谷歌推出的,这种格式的图像压缩算法能力要优于传统的 jpg
, png
等格式,在相同图片质量的情况下,空间大小会优化 30% 左右的样子
关于图片的性能优化就是小图用雪碧图,大图用 webp
格式
十二、webpack:tree-shaking | 打包文件名 + hash
webpack
一般来说都是配置好的,默认配置的中规中矩,不算差也不算好,我们可以在此基础上增加一个 tree-shaking
, tree-shaking
的作用就是帮我们把项目中无用的代码给找出来,比如我们调试用的 console.log
,其实 console.log
对浏览器的开销还是蛮大的
以及项目中打包后的文件名被拼接了一个哈西值,这样就能剔除上一次打包的内容,或者做一个动态的组件引入
像是打包文件生成的 dist
目录,里面的文件默认就会有 hash
值, vite
默认会有这个操作,但是其他的打包工具就不一定会有
在实际开发中,打包工具不是由自己决定的,公司会统一好,不可能别人用
webpack
,你一个人用vite
十三、尽量减少回流重绘
这个问题此前也详细讲过:输入url到页面渲染后半段:回流,重绘,优化【一次性带你搞明白】
回流(重排)就是计算布局,重绘就是给页面上色
尽量不用 js 去直接修改 css
我们可以看下下面两种情况
1 | arduino复制代码// 案例一 |
第一种方案就是直接修改 css
,第二种是添加类名。方案一会导致回流,方案二不会导致回流,因为添加类名并没有修改几何属性,它是间接交给了 css
,上面就说了, css
一般放在文件顶部,提前加载好了,因此浏览器已经准备好了,做好了回流这个计算,就是等你把类名加上去
display: none
当涉及需要对 dom
进行一系列的操作时,可以先利用 display: none
将 dom
脱离文档流,再修改 display: block
带回文档流
fragment
这是文档虚拟片段, js
中被当成 dom
,但是在 css
中又不会当成真实的 dom
加载出来,涉及多个操作 dom
时,可以对 fragment
操作,最后把 fragment
挂到真实 dom
上
clone
深克隆节点,对副本进行操作,最后将副本插入到文档中进行回流,跟前面的方法原理是一致的,具体语法上面晾出的文章都详细讲过
十四、事件委托
同样,事件委托之间也单独拿出来讲过 js事件委托 ,这里就不大费周章重复去讲
事件委托的机制是借助冒泡机制,把原本需要批量操作子组件的操作代理到一个父组件上
十五、if-else VS switch
当涉及到多个判断条件时,我们可以用 switch
去写判断语句
比如下面两个案例
1 | ini复制代码// 检查成绩等级(if-else) |
if-else
有个判断顺序的,一定是从上往下走逐个走到目标,每次都判断一下,浪费性能。而 switch
不然, switch
是直接命中目标,只有一次判断
if-else
会更加灵活,但是性能又没有 switch
来得好
十六、requestAnimationFrame避免页面卡顿
下面用 js
实现一个效果:小球滚动
用一个定时器 setInterval
,让 left
值每间隔 16ms
就去自增
1 | xml复制代码<!DOCTYPE html> |
上面展示的动画其实是有卡顿的,当然,光看这个效果肯定大部分是 gif
的原因,为何 16ms
也会有卡顿?屏幕设计成 60Hz
不就是因为人眼最低的时间辨别时间就是 16.7ms
么,确实,但是 16
和 16.7
就是不匹配,这个小球从第 A 位置移到 B 位置确实不会卡,但是下一次开始,移动的时候页面刷新一下,此时才会带来卡顿
这个时候我们就去采用 requestAnimationFrame
,这也是个定时器,这个定时器的执行时间不需要我们设置,它是根据屏幕刷新率来定的, 60Hz
就是 16.7ms
执行一次。
关于宏微任务,这个方法有歧义,暂且不讨论。总之这个方法的设计就是在每一帧渲染完准备去显示下一帧去执行的,它就是个动画帧
1 | ini复制代码let box = document.querySelector('.box') |
采用这个方法实现的动画将会非常丝滑,这里用 gif
不便展示其效果
十七、Web Worker 开启多线程
给
js
开多线程是这个话题中非常加分的点
js
默认情况下是单线程,但是 v8
引擎执行 js
的时候是可以开辟多线程的
像是页面上的图片有水印一般都是页面加载的时候实现的,而非图片本身就有水印,像这种操作就是交给另一个线程来实现的
接下来实现一个效果带你理解多线程:点击上传图片,将图片变成黑白色
1 | xml复制代码<!DOCTYPE html> |
代码先写到这里,这里代码还没有实现黑白效果。举个例子,以预览为例,预览对于用户而言,其实慢点是没有关系的,也就是说这个功能不应该阻塞线程,因此像是这样的逻辑,如果你正常往下实现,是会占用这个线程的。
或者说,假设你把像是预览图片的代码写在 js
主线程中,若预览需要 3s
,此时下面假设还有其他按钮,这 3s
内,你就无法进行其他操作了
接下来就把像是预览这样的操作交给第二个线程来实现,这里实现的是变黑白
1 | javascript复制代码render.onload = (e) => { |
postMessge
和 onmessge
上一次见过,可以用管道通信的方式实现深拷贝 让面试官眼前一亮的深拷贝
这两个方法是异步的,这个异步是针对主线程而言,但是里面的耗时是交给了另一个线程
接下来同级目录下新建一个文件 worker.js
,我们在这里面实现其他逻辑
我们可以试着在 worker.js
中打印 this
或者 self
看看指向, self
就是 this
,我们在这身上监听 message
事件
1 | javascript复制代码// console.log(self); |
我们打印 e
,你会发现主线程中的 imageBitmap
在里面,这就是因为 self
监听到了主线程 postMessage
发布过来的 imageBitmap
1 | javascript复制代码// console.log(self); |
processImage
就是将参数转成 Uint8Array
二进制数组
es8
新推出了一个 Uint8Array 就是专门用于存放数据流的格式
接下来实现 processImage
这个函数,这个函数需要做的就是将从主线程读到的文件流变成黑白,通过 canvas
实现,最终 worker.js
如下
1 | arduino复制代码// console.log(self); |
这些过程中用上 canvas
纯粹是因为 canvas
身上有个方法 toDataURL
可以把 image
转成真实的 url
供你放到 img
上展示,这个 url
其实就是 base64
格式
1 | ini复制代码createImageBitmap(new Blob([imageData])).then((imageBitmap) => { // 将buffer流转成真实的数组,且是异步方法 |
我们可以打印看下这个 url
,此时的图片已经 worker.js
处理回来了,黑白格式
最后把这个 base64
的 url
放入到 src
中即可
1 | ini复制代码previewImage.src = previewCanvas.toDataURL() |
面试官:如何将一个图片转成 base64 格式
顺带提这个面试题,看到之前有人被问到过
用 canvas
就可以实现
1 | ini复制代码let img = new Image(); // 创建一个新的 Image 对象 |
除了给图片变换其他效果需要开启多线程外,其余还有很多情景,比如一个 for
循环需要循环一万次产生了一个阻塞,我们就可以把这个任务交给另一个线程来实现
十八、 css 选择器复杂性要低
我们看下面这案例
1 | less复制代码#app .text p{ |
浏览器读取 css
是从右往左读,因此上面的代码,就是先读到 p
标签,再是 text
类名,最后是 id
这样就是先去找到所有的 p
标签,然后再去找带有 text
类名的部分,最后再去找还得带有 app
这个 id
的部分,这个工作量将会非常大,因此可以选择设置一个唯一的类名进行优化
1 | sql复制代码.only{ |
因此,尽量给每个标签打上类名,不要去通过父容器
当然,这样就需要耗费精力去取类名了
十九、尽量使用弹性布局
flexbox
性能会比之前的布局好, flexbox
之前的布局就有浮动,定位, flexbox
的性能是它们的四倍左右
最后
其实当被问到这个问题的时候,你内心应该是开心的,这个问题答案太多了,面试时间是有限的,你需要把握主动权
性能优化比较发散,可能你还清楚一些我不知道的手段,欢迎各位大佬进行补充
如果你对面试感兴趣,可以关注我的公众号:
Dolphin海豚
,可以加微信进面试群,讨论你面试过程中遇到的问题,我们一起解决
另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请 ”点赞+评论+收藏“ 一键三连,感谢支持!
本文转载自: 掘金