前端性能优化

576 阅读12分钟

六个角度的优化:资源优化、渲染优化、代码优化、传输优化、展示优化、webpack 构建优化……


一 资源优化

1. 压缩合并

请求资源的大小和 http 请求数是影响 web 性能的至关重要的因素。压缩合并的目的就是为了减少请求资源的大小、减少 http 请求数。

html、css、javascript 及图片资源等都有在线的压缩工具,也有相应的压缩插件,如 html-minifer、clean-css 等,如今通过 webpack 构建项目,可通过一个配置文件(webpack.config.js)统一处理各种文件的压缩合并。详情见 webpack 章节。

2. 图片选择

  • jpg/jpeg:压缩比高,体积小画质好,缺陷是有锯齿感、粗糙。有 imagemin 压缩工具。

  • png:没有 jpg 的缺点,画质细腻,可做透明背景的图片,但是体积较大,适合做小图片。有imagemin-pngquant 压缩工具。

  • webp:压缩比高,又有 png 的画质,缺点是目前浏览器兼容性不太好。

二 渲染优化

1. 渲染原理:关键渲染路径(critical rendering path)

关键渲染路径各阶段的加载可在 chrome 开发者工具 Performance 一栏中查看。

关键渲染路径2.png

(1)构建 DOM 树

当从服务器接收HTML页面的第一批数据时,DOM 解析器就开始工作了。

HTML 可以解析完成一部分内容呈现一部分,但是,其他资源如 CSS 和 JavaScript 会阻塞页面的渲染。

<html>  
<head>  
  <title>Understanding the Critical Rendering Path</title>
  <link rel="stylesheet" href="style.css">
</head>  
<body>  
  <header>
    <h1>Understanding the Critical Rendering Path</h1>
  </header>
  <main>
    <h2>Introduction</h2>
    <p>Lorem ipsum dolor sit amet</p>
  </main>
  <footer>
    <small>Copyright 2017</small>
  </footer>
</body>  
</html>

如上代码将会被构建为:

DOM.jpg

(2)构建 CSSOM 树

CSS 是一种渲染阻塞资源,必须全部解析之后才能进行渲染树的构建。

浏览器默认提供 UserAgent 样式,如果你不提供任何样式,默认使用的就是 UserAgent 样式。

body { font-size: 18px; }

header { color: plum; }  
h1 { font-size: 28px; }

main { color: firebrick; }  
h2 { font-size: 20px; }

footer { display: none; } 

如上代码将会被构建为:

CSSOM.jpg

运行 javascript

javascript 是一种阻塞解析的资源,当解析 HTML 文档自身时候会被 JavaScript 给阻塞掉。可以在 <script>标签中设置 async(异步)defer(延迟)避免解析阻塞。

当解析器解析到<script>标签时,无论该资源是内部还是外链的都会停止解析,并且等到资源被下载并运行结束后才继续进行解析。

(3)构建渲染树(Render Tree)

渲染树是 DOM 和 CSS 的结合体,是最终渲染在页面上的元素的结构对象。它只关注可见的内容,对于被隐藏或者 display:none 的元素,不会被包含在结构内。

rendertree.jpg

(4)生成布局(Layout)

根据渲染树构造布局树的位置顺序及大小等。

(5)图层(layer)与绘制(Paint)

页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做的 z 轴排序等,为了更加方便地实现这些效果,渲染引擎为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层:

  • 拥有层叠上下文属性的元素会被提升为单独的一层(position: fixed; opacity; z-index; filter)。
  • 需要剪裁(clip)的地方会被创建为图层

根据布局把页面拆分图层(layer),再绘制渲染。分图层的优点是后续可以对单个图层进行重排重绘或复合,缺点是太多会占用太多内存消耗性能。

图层的查看:chrome 开发者工具 ctrl + shift + p 调出输入框,搜索 layers

创建图层的 css 属性: will-change

(6)复合(Composite)

通常一个页面可能很大,但是用户只能看到其中的一部分,这个可见的部分叫做视口(viewport),这种情况下,没必要渲染出所有的内容,不然开销很大。

合成线程可以将图层划分为图块(tile), 并按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化(raster)来执行的。所谓栅格化,是指将图块转换为位图。

layer.webp

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

2. 回流重绘

回流重绘.jpg

(1)回流/重排(Reflow)

渲染树(render tree)中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建。

防止布局抖动工具(高频率触发回流,如获取offsetTop等信息): FastDom(读写分离)。

重排需要更新完整的渲染流水线,所以开销也是最大的,如图,每个步骤都会再执行一遍。

reflow.webp

(2)重绘(Repaint)

当渲染树(render tree)中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color。则就叫称为重绘。

重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

repaint.webp

(3)直接合成

渲染引擎将跳过布局和绘制,只执行后续的合成操作。如 css 的 transform, opacticy 属性,直接在非主线程上执行合成。相对于重绘和重排,合成能大大提升绘制效率。

hecheng.webp

3. 渲染层面的防抖

一帧的生命周期(life of a frame):

raf.png

触发事件时,js执行后发生视觉上的变化,当一帧开始时,会在 layout 和 paint 之前先触发 rAF 调用,而且 rAF 本身是由 JavaScript 来调度的,这样我们就可以在 rAF 中先将我们想要处理的做完后再进行布局和绘制,这样可以极大的提高效率。

// 代码示例
/**
 * 如果一帧之内多次触发 rAF,通过变量 ticking 控制
 */
let cards = document.getElementsByClassName('card');

// 修改图片宽度,触发重排
function changeWidth(position) {
    for(let i = 0; i< cards.length; i++) {
        cards[i].style.width = (Math.sin(position / 1000) + 1) * 500;
    }
}

// 使用rAF防抖
let ticking = false;
window.addEventListener('pointermove', (e) => {
    let pos = e.clientX;
    if(ticking) return;
    // 如果 ticking = true,说明已有一个 rAF 在执行
    // 该 rAF 执行完毕后,才会继续执行下一个 rAF
    ticking = true;
    window.requestAnimationFrame(() => {
        changeWidth(pos);
        ticking = false;
    })
})

三 代码优化

资源的加载渲染过程中尽可能减少阻塞的情况,一般 css 尽早加载, js 放到后面加载。

浏览器会对各种资源的加载有一定的优先级,默认情况下,浏览器会按照资源默认的优先级确定加载顺序:

  • html 、 css 、 font 这三种类型的资源优先级最高;

  • 然后是 preload 资源(通过 <link rel=“preload"> 标签预加载)、 script 、 xhr 请求;

  • 接着是图片、语音、视频;

  • 最低的是 prefetch 预读取的资源。

我们可以通过 preloadprefetch 来调整资源的加载顺序。

1. javascript 优化

(1)javascript 内存管理

javascript 的垃圾回收机制(GC)是自动管理的,变量创建时自动分配内存,变量没有引用时(判断变量是否还能再次被访问到),自动回收释放。

对于局部变量,当函数执行完毕,又无闭包引用时,就会回收;对于全局变量,会一直存在,直到浏览器卸载才会回收。

但是,不管是引用计数的方式,还是标记清除的方式(GC机制)都是“近似”实现,不是确保无误的。

javascript 避免内存泄漏的途径:

  • 避免意外的全局变量产生

  • 避免反复运行大量的闭包

  • 避免分离的 DOM 元素(分离的DOM元素是已从DOM中删除的元素,但是由于JavaScript的原因,它们的内存仍然保留)

(2)防抖节流

防抖节流的作用是减少事件函数的调用频率。

从渲染层面调用 rAF 防抖的方式,请参看上文。

防抖节流.jpg

防抖:当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

function debounce(fn,delay){
    let timer = null //借助闭包
    return function() {
        if(timer){
            clearTimeout(timer) 
        }
        timer = setTimeout(fn,delay) // 简化写法
    }
}

function showTop  () {...}
window.onscroll = debounce(showTop,1000)

节流:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

function throttle(fn,delay){
    let valid = true
    return function() {
       if(!valid){
           //休息时间
           return false 
       }
       // 工作时间
        valid = false
        setTimeout(() => {
            fn()
            valid = true;
        }, delay)
    }
}

function showTop  () {...}
window.onscroll = throttle(showTop,1000)

(3)事件委托与卸载

添加到页面上的事件处理程序越多,内存中的对象就越多,造成页面渲染和就绪时间延迟、事件不灵敏等性能问题。可以通过“事件委托”和适时移除事件的方式减少内存消耗,优化性能。

事件委托:利用事件冒泡的机制,指定一个事件处理程序来管理同一类型的所有事件。

<ul id="links">
    <li id="doSomething">doSomething</li>
    <li id="goSomewhere">goSomewhere</li>
    <li id="sayHi">say hi</li>
</ul>

var list = document.getElementById('links');
EventUtil.addHandler(list, 'click', function(e){
    e = EventUtil.getEvent(e);
    var target = EventUtil.getTarget(e);

    switch(target.id) {
        case 'doSomething':
            document.title = 'the document’s title was changed';
            break;
        case 'goSomewhere':
            location.href = 'http://www.baidu.com';
            break;
        case 'sayHi':
            alert('hi');
            break;
    }
})

适合采用事件委托的事件包括:click / mousedown / mouseup / keydown / keyup / keypress。

事件卸载:元素被删除或替换时,原来的事件并没有删除,而是继续滞留在内存里,造成内存的损耗。在处理 DOM 或卸载页面时,应该注意删除相关的事件。

(4)文档碎片

每次对 dom 的操作都会触发"重排",这严重影响到能耗,一般通常采取的做法是尽可能的减少 dom 操作来减少"重排"。

文档碎片document.createDocumentFragment()是一个容器,用于暂时存放需要操作的 dom 元素,然后一起操作,一次性渲染,从而减少重排。

// 普通方式:操作了 100 次 dom
for(var i=100; i>0; i--){ 
    var elem = document.createElement('div');
    document.body.appendChild(elem);
}

// 文档碎片:操作 1 次 dom     
 var df = document.createDocumentFragment();          
 for(var i=100; i>0; i--){              
      var elem = document.createElement('div');                             
      df.appendChild(elem);
 }               
document.body.appendChild(df);

2. html 优化

优化程度较小。

  • 减少 iframes 的使用。iframes 会阻塞主页的加载,必要时可通过延时加载,用 js 给 iframes 的 src 属性赋值。

  • 避免 table 布局。

  • css、js 文件尽量外链。

  • 避免节点深层次嵌套。

  • 压缩空白符、删除注释、删除元素默认属性、没必要闭合的元素不闭合。

3. css 优化

  • 尽早加载,降低 css 对渲染的阻塞。

  • 使用 contain 属性

    contain 属性声明的元素,相对于 DOM 树的其他部分是独立的,浏览器在重新计算布局、样式、绘图、大小或这四项的组合时,只影响到声明的 DOM 区域,而不是整个页面,可以有效改善性能。

  • 使用 flex 布局,flex 在性能上优于其他浮动定位等布局

  • Gpu 进行完成动画

4. 字体优化

字体在渲染过程中遇到的主要问题有FOIT(Flash Of Invisible Text)、FOUT(Flash Of Unstyled Text),是指字体未下载完成时,浏览器隐藏或自动降级,导致字体闪烁。这两个闪烁的问题是无法避免的,但是可以通过一定的方式去控制、优化闪烁的行为。

@font-face 中的 font-display 属性:

  • auto

  • block:阻塞,开始不展示文字,3s 后判断自定义字体是否下载完成,是则展示,否则展示默认字体

  • swap:替换,开始用默认字体展示,之后再替换。

  • fallback:开始不展示,100ms 后看自定义字体是否下载完成,最后替换为自定义字体。

  • optional:判断网络情况,100ms 后看自定义字体是否下载完成,一旦选择,不会再替换。

font-display.png

5. 图片加载优化

图片优化.png

  • 懒加载。 设置<img> 标签的 loading="lazy"> 属性; 该属性有一定的兼容性。第三方懒加载插件:Lozad.js,blazy,verlok/vanilla-lazyload。

  • 渐进式图片 渐进式图片的制作工具:progerssive-image,imageMagick,imagemin

  • 响应式图片 <img>标签的srcsetsizes属性,及<picture>标签。

四 传输优化

资源传输优化.jpg

在请求和响应资源传输的过程中对资源进行的优化。

  • Gzip:在传输阶段对资源进行压缩,需要对 nginx/tomcat/node 等进行一定的配置
  • http 请求头中加入 Connection: keep-alive,持久化 TCP 连接
  • http 资源缓存
  • Service Worker
  • http2
  • SSR 服务端渲染

五 展示优化

1. 首屏加载

首屏(above the fold),是指首页的上半部分,就是用户第一眼看到的部分。这部分是用户对应用的第一印象,十分重要。过长的白屏会影响用户的体验和留存。

用户加载体验的三个关键时刻:

  • First Contentful Paint(FCP)

  • Largest Contentul Paint(LCP)

  • Time to Interactive(TTI)

首屏.jpg

优化方法:

  • 资源:资源压缩,传输压缩,代码拆分,缓存,Tree shaking,Http2

  • 首页内容:资源懒加载,预渲染/SSR,Inline Css(内联 css)

  • 加载顺序:prefetch,preload

2. windowing 窗口化

只渲染可见的内容,渲染和滚动的性能都能得到很好的提升。

插件:react-windowvue-virtual-scroll-list

3. 骨架组件减少布局移动(Layout Shift)

使用骨架组件占位,等页面加载完成替换占位,布局不会有太大的变动,以提升用户感知性能。

骨架组件插件:react-placeholder

4. 预渲染

大型单页应用的性能瓶颈主要是 js 的下载及其解析执行,预渲染的作用是在打包时提前渲染页面,以提高页面渲染速度。

预渲染插件:react-snap

六 构建优化(webpack 优化)

webpack 优化