Skip to content

Latest commit

 

History

History
624 lines (422 loc) · 19.4 KB

前端性能优化.md

File metadata and controls

624 lines (422 loc) · 19.4 KB

页面性能

我们的页面需要交互流畅,交互流畅的判断标准是 60fps

60fps 是什么

目前大多数设备的屏幕刷新率为 60 次/秒,也就是 60fps , 如果刷新率降低,也就是说出现了掉帧, 对于用户来说,就是出现了卡顿的现象。

其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。

浏览器的渲染过程

当浏览器拿到了html 文件,浏览器会解析文档,生成dom树,这个过程被称为parse html

上面的html代码,会被解析为下面的dom树:

当浏览器知道某些规则应用于相关元素后,就开始计算布局,也就是计算元素会占用多少空间,这个过程被称为回流,或者布局,或者layout, 或者 reflow。layout阶段元素之间会相互影响,所以这个阶段对于浏览器来说是比较费时间的。

下一个阶段,被称为重绘,或者repaint,也就是从矢量到光栅,可以理解为浏览器需要在屏幕上把图像画出来,我们屏幕是由一个个像素点组成的,所以就像下面一样:

浏览器的渲染过程如下图所示,每一个渲染阶段我们都可以有针对性的进行优化。

下面是每一个阶段优化的方法:

js阶段的优化

使用requestAnimationFrame

setTimeout(callback)setInterval(callback) 无法保证 callback 函数的执行时机,如果恰好卡在16ms 即将渲染一次那一个时刻,则会导致这一帧丢掉。

requestAnimationFrame(callback)可以保证callback函数在每帧动画开始的时候执行。

// requestAnimationFrame将保证updateScreen函数在每帧的开始运行
requestAnimationFrame(updateScreen);  

长耗时js 放在web works

每帧的渲染应该在16ms内完成,JavaScript代码运行耗时应该控制在3-4毫秒。 特别耗时的纯计算工作,可以考虑放到Web Workers中执行。

var dataSortWorker = new Worker("sort-worker.js");

dataSortWorker.postMesssage(dataToSort);

// 主线程不受Web Workers线程干扰
dataSortWorker.addEventListener('message', function(evt) {
    var sortedData = e.data;

    // Web Workers线程执行结束
    // ...
});    

dom操作部分:分为多个小任务,放进frame中

对于很多需要操作DOM元素的逻辑,可以考虑分步处理,把任务分为若干个小任务,每个任务都放到requestAnimationFrame中回调执行

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);

requestAnimationFrame(processTaskList);

function processTaskList(taskStartTime) {
    var nextTask = taskList.pop();

    // 执行小任务
    processTask(nextTask);

    if (taskList.length > 0) {
        requestAnimationFrame(processTaskList);
    }
}     

style阶段的优化

style阶段确定每个DOM元素应该应用什么CSS规则。

过程:

  1. 根据css的规则建立一个树状索引。需要注意的是,css规则索引是从选择器的右侧开始。
  2. 遍历dom树,每一个dom节点都要走一边css索引,然后生成 parse tree

css匹配顺序

css规则索引是从选择器的右侧开始

div.container p {
  color: red
}

解析上面的css规则时,从右边开始,先找到所有的p标签,再匹配这些所有的p标签中其父元素的类名是container的那个p元素。

<div>
   <div class="container">
    <ul>
      <li class="item"></li>
      <li class="item"></li>
    </ul>
   </div>
   <ul>
     <li></li>
     <li></li>
   </ul>
</div>

对于上面的html结构,下面哪种css选择器效率更高呢?

// 第一种
.container ul li {

}

// 第二种
.item {

}

很明显是第二种。第一种的选择器会先匹配所有的li,然后筛选出所有的li中带有父节点ul的,然后再筛选出父节点的父节点类名带有 container 的那个。

css选择器的优化

降低样式选择器的复杂度,尽量保持class的简短

.box:nth-last-child(-n+1) .title {
}
// 改善后
.final-box-title {
}    

layout阶段的优化

我们能在不同的文章中看到不同的名词: 布局layout , 回流reflow , 这些名词说的都是一回事,不同浏览器的叫法不同

几乎任何测量元素的宽度,高度,和位置的方法都会不可避免的触发reflow, 包括但是不限于:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • and a lot more…

更详细的信息可以访问https://gist.github.com/paulirish/5d52fb081b3570c81e3a

布局的主要消耗性能的地方在于:1. 需要布局的DOM元素的数量;2. 布局过程的复杂程度

避免触发布局

下面是会触发layout的操作:

  1. 增删改动dom,比如说动画
  2. 修改css
  3. 修改默认字体
  4. resize 窗口

使用flexbox

老的布局模型以相对/绝对/浮动的方式将元素定位到屏幕上 Floxbox布局模型用流式布局的方式将元素定位到屏幕上,flex性能更好。

避免强制同步布局

从触发源来说,又有两种情形会触发layout:

  1. 写重排,即每次尝试给元素的这些属性赋值会引起layout: width height left top margin padding
  2. 读重排,即每次尝试读取这些值的时候就会引起layout: offsetTopoffsetLeftoffsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeightclientTopclientLeftclientWidthclientHeight 、(getComputedStyle() or currentStyle in IE)

写重排,浏览器不是立马执行的,而是先等一等,合并批量更新,但是任何一个读重排都会中断这个过程,浏览器会强制同步布局。

paint绘制阶段的优化

使用transform

使用transform不会触发layout , 只会触发paint。

如果你想页面中做一些比较炫酷的效果,相信我,transform可以满足你的需求。

// 位置的变换
transform: translate(1px,2px)

// 大小的变换
transform: scale(1.2)

composite 渲染层阶段

上文中说了,在浏览器中,页面内容是存储为由 Node 对象组成的树状结构,也就是 DOM 树。每一个 HTML element 元素都有一个 Node 对象与之对应。其实,从 DOM 树到最后的渲染,需要进行一些转换映射。

深究composite阶段

1. 从 Nodes 到 LayoutObjects

DOM 树每个 Node 节点都有一个对应的 LayoutObject 。LayoutObject 知道如何在屏幕上 paint Node 的内容。

2. 从 LayoutObjects 到 PaintLayers

有相同坐标的 LayoutObjects ,在同一个渲染层(PaintLayer)。PaintLayer 最初是用来实现 stacking contest (层叠上下文)。 根据创建 PaintLayer 的原因不同,可以将其分为常见的 3 类:

  • NormalPaintLayer

    • 根元素
    • relative、fixed、sticky、absolute
    • opacity 小于 1
    • CSS 滤镜(fliter)
    • 有 CSS mask 属性
    • 有 CSS mix-blend-mode 属性(不为 normal)
    • 有 CSS transform 属性(不为 none)
    • backface-visibility 属性为 hidden
    • 有 CSS reflection 属性
    • 有 CSS column-count 属性(不为 auto)或者 有 CSS column-width 属性(不为 auto)
    • 当前有对于 opacity、transform、fliter、backdrop-filter 应用动画
  • OverflowClipPaintLayer

    • overflow 不为 visible
  • NoPaintLayer

    • 不需要 paint 的 PaintLayer,比如一个没有视觉属性(背景、颜色、阴影等)的空 div。

4. 从 PaintLayers 到 GraphicsLayers

某些特殊的paintLayer会被当成合成层(Compositing Layers),合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。

每个 GraphicsLayer 都有一个 GraphicsContext,GraphicsContext 会负责输出该层的位图,位图是存储在共享内存中,作为纹理上传到 GPU 中,最后由 GPU 将多个位图进行合成,然后 draw 到屏幕上,此时,我们的页面也就展现到了屏幕上。

渲染层提升为合成层的原因有一下几种:

  • 直接原因

    • 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层
    • video元素
    • 3d transiform
    • 在 DPI 较高的屏幕上,fix 定位的元素会自动地被提升到合成层中。但在 DPI 较低的设备上却并非如此
    • backface-visibility 为 hidden
    • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要注意的是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效)
    • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)
  • 后代元素原因

    • 有合成层后代同时本身有 transform、opactiy(小于 1)、mask、fliter、reflection 属性
    • 有合成层后代同时本身 overflow 不为 visible(如果本身是因为明确的定位因素产生的 SelfPaintingLayer,则需要 z-index 不为 auto)
    • 有合成层后代同时本身 fixed 定位
    • 有 3D transfrom 的合成层后代同时本身有 preserves-3d 属性
    • 有 3D transfrom 的合成层后代同时本身有 perspective 属性
  • overlap 重叠原因

为啥overlap 重叠也会造成提升合成层渲染? 图层之间有重叠关系,需要按照顺序合并图层。

优化策略

使用 will-change 或者 transform3d

1. will-change: transform/opacity
 2. transform3d(0,0,0,)

composite更详尽的知识可以了解下面这个博客: 《GPU Accelerated Compositing in Chrome》 http://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome

接下来,我们亲自去改造一个页面, 这个页面的地址是: https://mp.beibei.com/imp/2017/12/kanjia.html#/list

优化左侧弹窗

弹窗的动画为:每隔3秒进行向左侧滑动淡出,然后再滑动重新淡入,更新文本为“**砍价9元”

之前的滑动和淡出的效果是通过vue提供的 <transision> 来实现的

<transision> 原理

当我们想要用到过渡效果,会在vue中写这样的代码:

<transition name="toggle">
  <div class="test">
</transition>

但是其实渲染到浏览器中的代码,会依次是下面这样的:

// 过渡进入开始的一瞬间
<div class="test toggle-enter">

// 过渡进入的中间阶段
<div class="test toggle-enter-active">

// 过渡进入的结束阶段
<div class="test toggle-enter-active toggle-enter-to">


// 过渡淡出开始的一瞬间
<div class="test toggle-leave">

// 过渡淡出的中间阶段
<div class="test toggle-leave-active">

// 过渡淡出的结束阶段
<div class="test toggle-leave-active toggle-leave-to">

也就是说,过渡效果的实现,是通过不停的修改、增加、删除该dom节点的class来实现。

<transision> 影响页面性能

一方面, v-if 会修改dom节点的结构,修改dom节点会造成浏览器重走一遍 layout 阶段,也就是重排。另一方面,dom节点的class被不停的修改,也会导致浏览器的重排现象,因此页面性能会比较大的受到影响。

若页面中 <transition> 控制的节点过多时,页面的性能就会比较受影响。

为了证明,下面代码模拟了一种极端的情况:

<div v-for="n in testArr">
  <transition name="toggle">
    <div class="info-block" v-if="isShow"></div>
  </transition>
</div>
  export default {
  	data () {
          return {
            isShow: false,
            testArr: 1000
          }
    },
    methods: {
	    toggle() {
	    	var self = this;
	    	setInterval(function () {
		      self.isShow = !self.isShow
	      }, 1000)
      }
    },
    mounted () {
	 this.toggle()
    }
  }
  .toggle-show-enter {
    transform: translate(-400px,0);
  }

  .toggle-show-enter-active {
    color: white;
  }

  .toggle-show-enter-to {
    transform: translate(0,0);
  }

  .toggle-show-leave {
    transform: translate(0,0);
  }

  .toggle-show-leave-to {
    transform: translate(-400px,0);
  }

  .toggle-show-leave-active {
     color: white;
  }

上面的代码在页面中渲染了 1000 个过渡的元素,这些元素会在1秒的时间内从左侧划入,然后划出。

此时,我们打开google浏览器的开发者工具,然后在 performance 一栏中记录分析性能,如下图所示:

可以发现,页面明显掉帧了。在7秒内,总共 scripting 的阶段为3秒, rendering 阶段为1956毫秒。

事实上,这种跑马灯式的重复式效果,通过 animation 的方式也可以轻松实现。 我们优化上面的代码,改为下面的代码,通过 animation 动画来控制过渡:

    <div v-for="n in testArr">
      <div class="info-block"></div>
    </div>
  export default {
  	data () {
  	  return {
            isShow: false,
            testArr: 1000
      }
    }
  }
.info-block {
  background-color: red;
  width: 300px;
  height: 100px;
  position: fixed;
  left: 10px;
  top: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: toggleShow 3s ease 0s infinite normal;
}

@keyframes toggleShow {
  0% {
    transform: translate(-400px);
  }
  10% {
    transform: translate(0,0);
  }
  80% {
    transform: translate(0,0);
  }
  100% {
    transform: translate(-400px);
  }
}

打开浏览器的开发者工具,可以在 performance 里面看到,页面性能有了惊人的提升:

为了进一步提升页面的性能,我们给过渡的元素增加一个 will-change 属性,该元素就会被提升到 合成层 用GPU单独渲染,这样页面性能就会有更大的提升。

为了更显著的看出 will-change 带来的性能提升,我们把页面中渲染的过渡节点提升到 10000 个。在节点没有添加 will-change 属性时,页面的fps在1~5左右徘徊。

未添加will-change

增加了 will-change 属性之后,fps稳定在15~20之间。

综上,我们使用 animation 代替 <transition> ,同时使用 will-change ,页面的性能有了非常明显的提升。

基于以上的思路,我们对https://mp.beibei.com/imp/2017/12/kanjia.html#/list 这个页面的代码尝试修改。

这个是修改之前的渲染结果:

这个是修改之后的渲染结果:

优化懒加载

该页面使用了懒加载,通过看懒加载库的代码,懒加载是通过绑定 scroll 事件一个回调事件,每一次调用一次回调事件,就会测量一次元素的位置,调用 getBoundingClientRect() 方法,从而计算出是否元素出现在了可视区。

// 懒加载库中的代码,判断是否进入了可视区
const isInView = (el, threshold) => {
  const {top, height} = el.getBoundingClientRect()
  return top < clientHeight + threshold && top + height > -threshold
}

scroll 造成页面性能下降

scroll 事件会被重复的触发,每触发一次就要测量一次元素的尺寸和位置。尽管对 scroll 的事件进行了节流的处理,但在低端安卓机上仍然会出现滑动不流畅的现象。

优化的思路是通过新增的api—— IntersectionObserver 来获取元素是否进入了可视区。

intersection observer

intersection observer api 可以去测量某一个dom节点和其他节点,甚至是viewport的距离。

这个是实验性的api,你应该查阅https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#Browser_compatibility 查看其兼容性

在过去,检测一个元素是否在可视区内,或者两个元素之间的距离如何,是一个非常艰巨的任务。 但获取这些信息是非常必要的:

  1. 用于懒加载
  2. 用于无限加载,就是微博那种刷到底接着请求新数据可以接着刷
  3. 检测广告的可见性

在过去,我们需要不断的调用 Element.getBoundingClientRect() 方法去获取到我们想拿到的信息,然而这些代码会造成性能问题。

intersection observer api 可以注册回调函数,当我们的目标元素,进入指定区域(比如说viewport,或者其他的元素)时,回调函数会被触发;

intersectionObserver 的语法

  var handleFun = function() {}
  var boxElement = document.getElementById()
  
  var options = {
    root: null,
    rootMargin: "0px",
    threshold: 0.01
  };

  observer = new IntersectionObserver(handleFunc, options);
  observer.observe(boxElement);

基于IntersectionObserver的懒加载的库

尝试封装了一个基于IntersectionObserver的懒加载的库。

html

<img class="J_lazy-load" data-imgsrc="burger.png">

你也许注意到上面的代码中,图片文件没有 src 属性么。这是因为它使用了称为 data-imgsrc 的 data 属性来指向图片源。我们将使用这来加载图片

js


function lazyLoad(domArr) {
	if ('IntersectionObserver' in window) {
		
		let createObserver = (dom) => {
			var fn = (arr) => {
				let target = arr[0].target
				if (arr[0].isIntersecting) {
					let imgsrc = target.dataset.imgsrc
					if (imgsrc) {
						target.setAttribute('src', imgsrc)
					}
					
					// 解除绑定观察
					observer.unobserve(dom)
				}
			}
			
			var config = {
				root: null,
				rootMargin: '10px',
				threshold: 0.01
			}
			
			var observer =  new IntersectionObserver(fn, config)
			observer.observe(dom)
		}
		
		Array.prototype.slice(domArr)
		domArr.forEach(dom => {
			createObserver(dom)
		})
	}
}

这个库的使用也非常简单:

// 先引入
import {lazyLoad} from '../util/lazyload.js'

// 进行懒加载
let domArr = document.querySelectorAll('.J_lazy-load')
lazyLoad(domArr)

然后测试一下,发现可以正常使用:

这个动图太大了加载不出来,sad http://h0.hucdn.com/open/201825/9e5dfa5954ac4545_1604x1300.gif

比较性能

传统的懒加载 lazy-loder 的页面性能如下:

在12秒内,存在红颜色的掉帧现象,一些地方的帧率偏低(在devtool里面是fps的绿色小山较高的地方),用于 scripting 阶段的总共有600多ms.

使用intersetctionObserver之后的懒加载性能如下:

在12秒内,帧率比较平稳,用于 scripting 阶段的时间只有60多ms了。