性能的考量:第一回(方法篇)
本文旨在梳理目下通用的性能优化方法
性能优化没有一个统一的指标,但目的是明确的,就是要让站点应用的加载速度够快,如此用户体验才能高(暂且不管页面做的烂不烂),毕竟从应用开发者的角度看,时间就是金钱。为了让时间压缩到极致,我们需要从请求到完全响应的全部细节入手,尽可能的压缩。即:从用户在地址栏中输入—>页面加载完成,该过程中的每一个点,都需要倍加注意。可能这就是为什么那么多面试官那么喜欢问一道烂大街的题: 地址栏里输入地址后到直到页面展示,尽可能详细地说说…
精髓部分
根据《高性能浏览器网络》的研究结论得出,性能优化三个大的角度:带宽、延时和渲染耗时.那么由于现代光纤通信的大规模普及,带宽的影响因素,已经小到可以忽略不计的地步(管道宽到你怎么都塞不满),所以本文不讨论,剩下的就是延时和渲染。
- 延迟耗时层面
《计算机网络》的第六版中,对于延迟给出了明确的定义,从c端发出消息到s端收到消息的时间消耗,具体包括:排队延迟 –(缓冲)-> 处理延迟–(路由)->传输延迟—>传播延迟。但是扯了这么多是基于一个大前提的,就是真实发送请求的情况下。引用《高性能浏览器网络》的终极解决思路:没有请求,就是最快的。
有点废话的意思,但至少也是个思路,能不发则不发,能少发就少发。依据这一原则,我们可以有如下的措施:
- 能不发就不发。既然要不发送请求,那就是直接从本地读取。所以要使用
缓存策略,具体实践:开启cache-control,缓存资源。 - 能少则少。所以就极尽能事的合并请求,具体实践:
精灵图整合众多小图资源等。 - 能近则近。我们不能让数据传输得更快,但可以让它们传输的距离更短-
CDN。同样大小的资源,距离用户越近,耗时理论上越小(网速差距不大) - 能小则小。
首部压缩,后端gzip压缩,减少资源重量,加快传输速度。 - 对于一些小的资源比如小图,可以通过
base64编码将其变成字符串直接嵌入到页面中,也不用发送请求。 - 甚至说,对于一些需要返回百万级别数据量的接口(项目中就碰到过如此奇葩的需求),我们可以通过传文件压缩包的形式给到前端,然后前端自行解压缩获取其中的数据。重点是要尽可能地把数据压扁。
- 坚决屏蔽重定向。
- 渲染层面
1. script标签defer或者async。由于在解析html时若碰到js代码就会牵扯到进程切换带来的开销且会阻塞html的解析,因此通过这俩属性规避之。
2. 减少重排重绘带来的不必要的开销。
- 防抖、节流该加的加上,屏蔽用户无脑操作。
- 图形化编程时能少画的就少画,能不画就不画。
- 直接用js改样式的炸裂操作,少做
3. 虚拟列表实现思路: 设置监听器监听用户是否滚动到底部,或者顶部。如果触发,当前数据的最后一项以参数传给后端,后端根据参数,返回对应的后面数据, 前端拿到拼接渲染。当然,也可以纯前端实现,有现成的库可供调用,如react-virtualized、react-window等。
4. 图片压缩、预加载、懒加载
压缩自不必说,能压则压,目下前端层面推荐格式webp。预加载:说白了就是提前加载。我们对目标图片预先请求一次,这样后面再次请求时,就会走本地缓存。图片懒加载:这个东西如何实现的呢?思路: 当图片item出现在可视范围内,再去加载图片。实现:- img标签的loading属性,设置为lazy。但是这种方式存在小瑕疵,当图片在靠近可视范围但还没有出现在可视范围内时, 图片会预先加载,就是不精确。
- 监听
scroll事件 - intersectionObserver
// 监听scroll事件 const iniObserver = () => { if (!myRef.current) { return; } console.log(myRef.current); // 这是原生的DOM对象 let options = { threshold: 1.0, }; const observer = new IntersectionObserver((entries) => { entries.forEach((entry: any) => { if (entry.isIntersecting) { entry.target.style.opacity = 1; } else { entry.target.style.opacity = 0; } }); }, options); observer.observe(myRef.current); };相当于设置一个监听,dom出现在视图内isIntersecting即为true。此时再去执行后续的逻辑
其他方面
DNS预解析甚至tcp链路预链接。
假设页面中有几个超链接。用户没有点击的时候我就已经预解析了域名ip甚至链路都连接好了,这样,当用户点的时候,只剩下发送资源请求的耗时。长连接。减少建立链路的耗时。这是浏览器层面的优化(无需关注底层已实现)
截止到http1.0版本的协议。请求响应都是遵循请求、响应、断开。周而复始。1.1版本最大的优化就是实现长链接,复用链路。
3. 分析包的大小
// 安装
cnpm install source-map-explorer
// 配置命令
"analyze": "source-map-explorer 'build/static/js/*.js'"
- React专属优化手段
类组件的shouldComponentUpdate
shouldComponentUpdate(nextprops, nextstate) {
if (nextprops.text === this.props.text) {
return false
} else {
return true
}
}
通过在shouldComponentUpdate生命周期中的判断逻辑,确定是否重新渲染, 由code可知,分为了false(不渲染)和true(重新渲染)两种
类组件中的pureComponent
相当于帮我们自动进行了比较的操作,但是注意,这里的比较,是浅比较。此时,需要结合Immutable.js,完全捕获变化,以确定是否更新。函数组件的React.memo
这个类似类组件的pureComponent
这个机制的作用在于,让子组件只有在props变化了的情况下,才会重新渲染。否则,每次父组件状态变化重新渲染,子组件会跟着操作。
// 父组件memo包裹
import { useEffect, useMemo, useState, memo } from 'react';
import ChildComponent from './ChildrenComponent';
const MemoSon = memo(ChildComponent)
...
...
<MemoSon name={name}/>
// 子组件
const ChildComponent = (props) => {
const { name } = props
console.log('儿子组件渲染')
return (
<div>
<div>我是{name}...</div>
</div>
)
}
export default ChildComponent
效果如下:
函数组件的useMemo
更加精细化的复用组件的某一个或某几个部分const Child = (props) => { const { count } = props console.log('儿子A渲染1') const content = useMemo(() => { console.log('儿子A渲染2') return count }, [count]) return ( <div> { content.name } </div> ) }
