持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
网络传输
开启HTTP/2
- 多路复用: 多个请求复用一次网络请求 -> 并发
- 二进制传输: 解析更高效、体积更小、不易出错
- 头部压缩: 索引字典 + 哈夫曼编码
- 服务端推送: 推送首屏需要的 JS/CSS/IMAGE 之类的资源
HTTP1.1 下的资源优化已经不适用
-
使用base64 进行资源内联
base64 编码后的资源体积会大于原来的资源体积
-
使用、进行资源内联
在 HTTP1.1 中建议内联首屏需要的样式,但是在HTTP/2中有服务端推送,首屏的CSS和JS直接通过服务端推送,实现并发;并且并且这些资源显然是可以缓存的,如果在HTML中内联,一般的HTML文件是不走缓存过的,也会导致缓存失效的问题。
-
使用粗粒度的资源整理,比如整合个JS / CSS / 以及著名的雪碧图
在多路复用下,这些操作显然没有必要了,而且考虑到资源的缓存,如果粒度粗会导致其中一部分文件的变更会让整个缓存失效
从客户端声明需要服务端推送
在 HTTP 头部声明字段
Link: </css/styles.min.css>; rel=preload; as=style
看起来和资源预加载有点像,但是这里是放在HTTP报文中的,并不是放在 link 标签中的
使用CDN部署静态资源
- 缓存时间
- 缓存刷新
- 缓存预热
使用DNS预解析
可以解析CDN或者服务端的地址
<link rel="dns-prefetch" href="//example.com">
提前建立网络链接
可以解析CDN或者服务端的地址
可以节省 DNS / TCP / TLS 等步骤
<link rel="preconnect" herf="//example.com">
<link rel="preconnect" herf="//example.com" crossorign>
域名收敛
配合HTTP2的多路复用的特性,减少 DNS / TCP / TLS 的建立链接的成本
使用 Brotli 压缩算法
相比 gzip 具有更好的压缩效果,评价压缩的好坏一般有两个维度
- 压缩 / 解压 的CPU资源
- 压缩后的体积(压缩率)
客户端支持 brotli
accept-encoding:gzip, deflate, br
这是飞书文档的请求标头,br则代表支持 Brotli 编码
服务端支持 brotli
-
需要 HTTPS / SSL
-
Npm i shrink-ray
app.use(shrink-ray({ cache:()=false, brotli:{ quality:11 } }))
可以有效提高 TTFB(Time to First Byte)
缓存
DNS缓存
暂时无法在飞书文档外展示此内容
浏览器缓存
比较容易和 HTTP缓存混淆,可以理解成HTTP缓存在浏览器的实现方案
- Memory 缓存:内存缓存,响应速度快,体量小,生命周期短
- Disk 缓存:磁盘缓存,响应速度慢,体量大,生命周期长
按照缓存顺序来讲,当一个资源准备加载时,浏览器会根据其三级缓存原理进行判断。
- 浏览器会率先查找内存缓存,如果资源在内存中存在,那么直接从内存中加载
- 如果内存中没找到,接下去会去磁盘中查找,找到便从磁盘中获取
- 如果磁盘中也没有找到,那么就进行网络请求,并将请求后符合条件的资源存入内存和磁盘中
运行时缓存
- Cookie
- Storage
- DB
- Memory
静态资源加载
资源缓存
资源预加载
- **link preload :**来声明当前页面所需要的核心资源,关键字体、样式、脚本等资源; 如果不支持 link preload, 可以使用 new Image().src = "xxx" 来发起资源请求
- link prefetch :预加载下级页面的资源
- link prerender: 发起二级页面的提前请求和页面渲染
JS 加载
异步加载
- 压缩文件大小:Terser ,Brotli/Gzip
- 按需加载:Code Splitting
- 精准Polyfill:按照宿主环境,只注入需要的Polyfill
- Tree Shaking
CSS加载
加载实现
- CSS In JS
- HTML内联Style
- Link
优化思路
- cssnano压缩代码 / Brotli / Gzip
- 精准 prefix
- 去除冗余样式 (PurifyCss)
图片加载
控制加载时机
- 关键图片:大图预加载 / 小图内联(base64/ svg)
- 次要图片: 大图懒加载(非首屏)/ 小图内联(base64/ svg)
使用合适的图片格式
优先使用WebP,并且使用 标签进行优雅降级
<picture>
<source srcset="img/logo.webp" />
<source srcset="img/logo.png"/>
<img src="img/logo.png"/>
</picture>
图片传输
响应式图片
- 对于背景图片
- @media screen and (min-width: xxpx)
- Background-image
- 对于 img 标签
<img src = "image-small.jpg"
srcset="imgae-medium.jpg 640w, image-big.jpg 1280w">
还可以使用 picture 进行更加细致的划分
<picture>
<source media = "(min-width: 704px)" srcset="a.jpg" sizes="33.3vw">
<source src="b.jpg" sizes="33.3vw">
</picture>
为什么有时候我们会嘲讽 " 你这个图都变绿了" 来指代这个图片很老
因为我们在传输图片数据的时候,很有可能传输的是压缩后的数据,那经常传输就会带来频繁的压缩,然后丢失数据 -> 图变绿了
- TinyPNG:在线压缩
- Webpack使用 img-optimize-loader
其他
- 使用媒体查询,加载不同图片
- 图片缩略图代理
字体加载
加载时机
内联是指:字体文件也可以转成base64
加载策略
- auto:默认值,在大多数浏览器中,类似于 block
- block:试图阻塞文本渲染,不展示备用的(空白),一段时间内如果加载好了就用新字体,不然才备用的
- swap: 先展示备用的(回退文字),加载好了再替换
- fallback:介于auto 和 swap 中间,短时间内(100毫秒)文本不可见 -> 展示回退文字 -> 加载完毕后可以切换
- Optional: 几乎和 fallback 一样,只是浏览器有更大的自由决定是否下载或应用字体
使用CSS控制字体加载优先级
@font-face{
font-family: name;
font-weight: 400;
font-style: normal;
src: the best font file, .... , the worst file
}
判断字体好坏一般有两个维度:浏览器支持程度 ,文件体积
document.fonts : FontFaceSet
FontFace:表示一个可用字体
let font = new FontFace('MyFont', 'url(myFont.woff2)');
/*
font 包含了一票字体相关的属性:wight / style / family
font.status = "unloaded", "loading", "loaded", "error"
font.loaded = 加载成功或者失败的时候会返回一个Promise,这里可以写回调
*/
FontFaceSet.add(font: FontFace)
let font = new FontFace('MyFont', 'url(myFont.woff2)');
document.fonts.add(font);
// 此刻的 字体就可以被使用啦
用途:实现字体的懒加载
这里要注意,可以用 Cookie / Storage 之类的存储用户之前是否已经加载过这个字体,不然的话会重复加载哦
代码运行时
React性能优化
跳过不必要的更新
-
用 React.memo 包裹函数组件,对 props 进行 浅层比较
-
由于 memo进行的是浅比较,所以如果 props 中传递的是对象或者函数的时候,尽管内容没变,但是传递的都是新的引用,所以对于 memo 的 Props 中,需要传递经过 useMemo / useCallback 生成的稳定的对象值
-
避免多层次的 props 传递,例如 A->B->C, A传递了一个值到C,B莫名其妙就被影响了,发生了 Re-render;
解决方案:context,redux 这些可以实现跨层级传递参数, 实现 发布订阅
JS优化
- 组件按需加载
- 懒加载:从路由 A 跳转到 B,通过 Webpack 的动态导入和
React.lazy方法,需要对懒加载失败做容错处理 - 懒渲染: Observer , react-visibility-observer
- 虚拟列表: react-window
- 批量更新
- 面试题: setState 是异步还是同步
答案是:在 React 管理的事件回调和生命周期中,setState 是异步的,而其他时候 setState 都是同步的。这个问题根本原因就是 React 在自己管理的事件回调和生命周期中,对于 setState 是批量更新的,而在其他时候是立即更新的。
而对于函数组件,则不存在这个问题,函数组件中生成的函数是通过闭包引用了 state,而不是通过 this.state 的方式引用 state,所以函数组件的处理函数中 state 一定是旧值,不可能是新值。
- debounce、throttle 优化频繁触发的回调