前端性能优化

132 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 缓存:磁盘缓存,响应速度慢,体量大,生命周期长

按照缓存顺序来讲,当一个资源准备加载时,浏览器会根据其三级缓存原理进行判断。

  1. 浏览器会率先查找内存缓存,如果资源在内存中存在,那么直接从内存中加载
  2. 如果内存中没找到,接下去会去磁盘中查找,找到便从磁盘中获取
  3. 如果磁盘中也没有找到,那么就进行网络请求,并将请求后符合条件的资源存入内存和磁盘中

运行时缓存

  • 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加载

加载实现

  1. CSS In JS
  2. HTML内联Style
  3. 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>

图片传输

响应式图片

  1. 对于背景图片
  • @media screen and (min-width: xxpx)
  • Background-image
  1. 对于 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性能优化

跳过不必要的更新

  1. 用 React.memo 包裹函数组件,对 props 进行 浅层比较

  2. 由于 memo进行的是浅比较,所以如果 props 中传递的是对象或者函数的时候,尽管内容没变,但是传递的都是新的引用,所以对于 memo 的 Props 中,需要传递经过 useMemo / useCallback 生成的稳定的对象值

  3. 避免多层次的 props 传递,例如 A->B->C, A传递了一个值到C,B莫名其妙就被影响了,发生了 Re-render;

    解决方案:context,redux 这些可以实现跨层级传递参数, 实现 发布订阅

JS优化

  1. 组件按需加载
  • 懒加载:从路由 A 跳转到 B,通过 Webpack 的动态导入和 React.lazy 方法,需要对懒加载失败做容错处理
  • 懒渲染: Observer , react-visibility-observer
  • 虚拟列表: react-window
  1. 批量更新
  • 面试题: setState 是异步还是同步

答案是:在 React 管理的事件回调和生命周期中,setState 是异步的,而其他时候 setState 都是同步的。这个问题根本原因就是 React 在自己管理的事件回调和生命周期中,对于 setState 是批量更新的,而在其他时候是立即更新的。

github.com/facebook/re…

而对于函数组件,则不存在这个问题,函数组件中生成的函数是通过闭包引用了 state,而不是通过 this.state 的方式引用 state,所以函数组件的处理函数中 state 一定是旧值,不可能是新值。

  1. debounce、throttle 优化频繁触发的回调

使用 requestAnimationFrame