如何提升scroll事件的性能

1. chrome devtool 是诊断页面滚动性能的有效工具javascript

2. 提高滚动时性能,就是要达到fps高且稳。css

3. 具体能够从如下方面着手html

  • 使用web worker分离无页面渲染无关的逻辑计算
  • 触发监听事件时使用函数节流与函数去抖
  • 使用requestAnimationFrame与requestIdleCallback代替定时器
  • 避免强制重排
  • 提高合成层

场景

滚动行为无时无刻不出如今咱们浏览网页的行为中,在许多场景中,咱们有有意识地、主动地去使用滚动操做,好比:前端

  • 懒加载
  • loadmore
  • affix
  • 回到顶部

以上场景伴随着滚动事件的监听操做,一不留神可能就让页面的滚动再也不“如丝般顺滑”。java

不择手段打造一个卡顿的scroll场景:git

 

 

做为一名优秀的前端工程师(将来的),怎么能允许出现这种状况!不就性能优化吗,撩起袖子就是干!github


原理

在一个流畅的页面变化效果中(动画或滚动),渲染帧,指的是浏览器从js执行到paint的一次绘制过程,帧与帧之间快速地切换,因为人眼的残像错觉,就造成了动画的效果。那么这个“快速”,要达到多少才合适呢?web

咱们都知道,下层建筑决定了上层建筑。受限于目前大多数屏幕的刷新频率——60次/s,浏览器的渲染更新的页面的标准帧率也为60次/s--60FPS(frames/per second)。算法

  • 高于这个数字,在一次屏幕刷新的时间间隔16.7ms(1/60)内,就算浏览器渲染了屡次页面,屏幕也只刷新一次,这就形成了性能的浪费。
  • 低于这个数字,帧率降低,人眼就可能捕捉到两帧之间变化的滞涩与突兀,表如今屏幕上,就是页面的抖动,你们一般称之为卡顿

来个比喻。快递天天整理包裹,并一天一送。若是某天包裹太多,整理花费了太多时间,来不及当日(帧)送到收件人处,那就延期了(丢帧)。chrome

那么在这16.7ms以内,浏览器都干了什么呢?

浏览器心里OS:不要老抱怨我延期(丢帧),我也很忙的好伐?

 

帧维度解释帧渲染过程

浏览器渲染页面的Renderer进程里,涉及到了两个线程,两者之间经过名为Commit的消息保持同步:

  • Main线程:浏览器渲染的主要执行步骤,包含从JS执行到Composite合成的一系列操做(下文会介绍)
  • Compositor线程:接收用户的一些交互操做(好比滚动) => 唤起Main线程进行操做 => 接收Main线程的操做结果 => commit给真正把页面draw到屏幕上的GPU进程

标准渲染帧:

在一个标准帧渲染时间16.7ms以内,浏览器须要完成Main线程的操做,并commit给Compositor进程

丢帧:

主线程里操做太多,耗时长,commit的时间被推迟,浏览器来不及将页面draw到屏幕,这就丢失了一帧

那么Main线程里都有些什么操做会致使延时呢?

 

进一步解释浏览器主要执行步骤

  • JavaScript:包含与视觉变化效果相关的js操做。包括并不限于:dom更新、元素样式动态改变、jQuery的animate函数等。
  • Style:样式计算。这个过程,浏览器根据css选择器计算哪些元素应该应用哪些规则,而后将样式规则落实到每一个元素上去,肯定每一个元素具体的样式。
  • Layout:布局。在知道对一个元素应用哪些规则以后,浏览器便可开始计算它要占据的空间大小及其在屏幕的位置。
  • Painting:绘制。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每一个可视部分。绘制通常是在多个表面(一般称为层)上完成的。(paint和draw的区别:paint是把内容填充到页面,而draw是把页面反映到屏幕上)
  • Composite:合成。因为页面的各部分可能被绘制到多层,由此它们须要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另外一元素重叠的元素来讲,这点特别重要,由于一个错误可能使一个元素错误地出如今另外一个元素的上层。

理论上,每次标准的渲染,浏览器Main线程须要执行JavaScript => Style => Layout => Paint => Composite五个步骤,可是实际上,要分场景。

指路官网

 

再进一步解释浏览器渲染流程

 

流程:

1.Compositor线程接收一个vsync信号,表示这一帧开始

2.Compositor线程接收用户的交互输入(好比touchmove、scroll、click等)。而后commit给Main线程,这里有两点规则须要注意:

  • 并非全部event都会commit给Main线程,部分操做好比单纯的滚动事件,打字等输入,不须要执行JS,也没有须要重绘的场景,Compositor线程就本身处理了,无需请求Main线程
  • 一样的事件类型,不论一帧内被Compositor线程接收多少次,实际上commit给Main线程的,只会是一次,意味着也只会被执行一次。(HTML5标准里scroll事件是每帧触发一次)

3.Main线程执行从JavaScript到Composite的过程,也有两点须要注意:

  • 注意红线,意思是可能会在JS里强制重排,当访问scrollWidth系列、clientHeight系列、offsetTop系列、ComputedStyle等属性时,会触发这个效果,致使Style和Layout前移到JS代码执行过程当中。
  • 实际上图中省略了Renderer进程中的其余线程,好比当Main线程走到js执行这一步时,会调起单独的js线程来执行。另外还有如HTML解释线程等。

4.当Main线程完成最后合成以后,与Compositor线程使用commit进行通讯,Compositor调起Compositor Tile Work(s)来辅助处理页面。Rasterize意为光栅化,想深刻了解什么是光栅的小伙伴能够戳这里了解:浏览器渲染详细过程:重绘、重排和composite只是冰山一角

5.页面paint结束以后,这一帧就结束了。GPU进程里的GPU线程负责把Renderer进程操做好的页面,交由GPU,调用GPU内方法,由GPU把页面draw到屏幕上。

6.屏幕刷新,咱们就在浏览器(屏幕)上看到了新页面。

接下来,简要介绍一下,如何使用chrome devtool分析页面性能。

示意图(chrome version: 61):

  • 帧率概览。看顶端绿色长条,越高表明帧率越高,高低起伏多表明帧率变化不稳定,越坑坑洼洼表明容易产生视觉上的卡顿。
  • 分析具体某一帧。若是发现,有哪一帧帧率特别低,能够在中间那一栏找到耗时长的那一帧,点击进行具体的活动分析。
  • 分析个活动耗时。自由选择某一段或某一帧观察这段时间内各项活动的耗时来诊断页面。(注意颜色)

应该注意,咱们能够看见,不多有帧的时间准确卡在了16.7s,实际上每帧达到60fps的帧率,只是一个理想化的数字,浏览器执行过程当中可能受到各类状况的干扰。而咱们人眼也没有那么灵敏,只要达到20帧以上,页面看起来就比较流畅了。尤为是结构复杂,数据较多的页面,盲目追求60fps只是钻牛角尖。因此,以我浅见,稳定的fps更能影响scroll效果。

关于更加具体地如何使用chome devtool分析页面性能,戳:Performance Analysis Reference


解决方案

咱们的目标很明确,就是拒绝卡顿!具体说来就是尽可能赶在16.7ms以内让浏览器完成五项工做,压缩每一个步骤时间。

使用web worker

当咱们了解了浏览器渲染时执行的过程,而且清楚浏览器内核处理方式(处理js的线程与GUI页面渲染线程互斥)以后,咱们很容易假想出这样一种情况:若是js大量的计算和逻辑操做霸占着浏览器,使页面渲染得不处处理,怎么办?

这种状况,很容易形成scroll的卡顿,甚至浏览器假死,就像alert()出现同样。

想象一下吧,原本你们好好地按照生理周期一个接一个上厕所,忽然小j便秘了!你说排在他后面的小g急不急,可急死了!

web worker是什么?

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程能够执行任务而不干扰用户界面。

这就好像,给容易“便秘”的小j,单独搭了个简易厕所。

之因此说这是一个简易厕所,由于它有一些限制

  • 没法访问DOM节点
  • 没法访问全局变量或是全局函数
  • 没法调用alert()或者confirm之类的函数
  • 没法访问window、document之类的浏览器全局变量
主线程和 worker 线程之间经过这样的方式互相传输信息:两端都使用 postMessage() 方法来发送信息, 而且经过 onmessage 这个 event handler来接收信息。 (传递的信息包含在 Message 这个事件的数据属性内) 。数据的交互是经过传递副本,而不是直接共享数据。

使用案例 - 判断素数

案例来自Web Workers, for a responsive JavaScript application

素数,定义为在大于1的天然数中,除了1和它自己之外再也不有其余因数。判断算法为,以2到它的平方根为界取整数作循环判断,用它和这个数字求余数,只要中间任意一次计算获得余数为零,则可以确认这个数字不是质数。

code

// in html
<script type="text/javascript">
// we will use this function in-line in this page
function isPrime(number)
{
    if (number === 0 || number === 1) {
        return true;
    }
    var i;
    for (i = 2; i <= Math.sqrt(number); i++) {
        if (number % i === 0) {
            return false;
        }
    }
    return true;
}

// a large number, so that the computation time is sensible
var number = "1000001111111111";
// including the worker's code
var w = new Worker('webworkers.js');
// the callback for the worker to call
w.onmessage = function(e) {
    if (e.data) {
        alert(number + ' is prime. Now I\'ll try calculating without a web worker.');
        var result = isPrime(number);
        if (result) {
            alert('I am sure, it is prime. ');
        }
    } else {
        alert(number + ' is not prime.');
    }
};
// sending a message to the worker in order to start it
w.postMessage(number);

</script>
<p style="height: 200px; width: 400px; overflow: scroll;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce blandit tristique risus, a rhoncus nisl posuere sed. Praesent vel risus turpis, et fermentum lectus. Ut lacinia nunc dui. Sed a velit orci. Maecenas quis diam neque. Vestibulum id arcu purus, quis cursus arcu. Etiam luctus, risus eu scelerisque scelerisque, sapien felis tincidunt ante, vel pellentesque eros nunc at magna. Nam tincidunt mattis velit ut condimentum. Vivamus ipsum ipsum, venenatis vitae placerat eu, convallis quis metus. Quisque tortor sapien, dapibus non vehicula quis, dapibus at purus. Nunc posuere, ligula sed facilisis sagittis, justo massa placerat nulla, nec pellentesque libero erat ut ligula. Aenean molestie, urna quis molestie auctor, lorem purus hendrerit nisi, vitae tincidunt metus massa et dolor. Sed leo velit, iaculis tristique elementum tincidunt, ornare et tellus. Quisque lacinia felis at est faucibus in facilisis dui consectetur. Phasellus sed ante id tortor pretium ornare. Aliquam ante justo, aliquam ut mollis semper, mattis sit amet urna. Pellentesque placerat, diam nec consectetur blandit, libero metus placerat massa, quis mattis metus metus nec lorem.
</p>

// in webworkers.js
function isPrime(number)
{
    if (number === 0 || number === 1) {
        return true;
    }
    var i;
    for (i = 2; i <= Math.sqrt(number); i++) {
        if (number % i === 0) {
            return false;
        }
    }
    return true;
}

// this is the point of entry for the workers
onmessage = function(e) {
    // you can support different messages by checking the e.data value
    number = e.data;
    result = isPrime(number);
    // calling back the main thread
    postMessage(result);
};

 

代码说明:

  • 使用web worker对一个较大数字(1000001111111111)进行素数判断
  • 获得结果以后alert(number + ' is prime. Now I'll try calculating without a web worker.')
  • 在不使用web worker的状况下,对相同数字进行素数判断,完成后alert('I am sure, it is prime. ')
  • 从页面标签里的内容的滚动状况判断两次计算对浏览器/页面形成的影响

现场还原:

不动戳我

案例总结:
从两次alert以后的段落滚动状况(第二次根本动不了),足以看出大量繁杂的js计算对页面的影响。恰当地使用web worker,能有效缓解页面scroll阻塞的状况。

并且它的支持率也良好~

 

在应用方面,Angular已经作了一些尝试。

解密Angular WebWorker Renderer (一):想办法打破web worker自己不能操做dom元素等限制,利用web worker执行渲染操做

Learn more about web worker

 

函数节流与函数去抖

针对scroll事件中的回调,思路之一是对事件进行“稀释”,减小事件回调的执行次数。

这就涉及到两个概念:函数节流和函数去抖

  • 函数节流(throttle):让函数在指定的时间段内周期性地间断执行
  • 函数去抖(debounce):让函数只有在过完一段时间后而且该段时间内不被调用才会被执行

有人这样比喻:

就像一窝蜂的人去排队看演出,队伍很乱,看门的老大爷每隔1秒,让进一我的,这个叫throttle,若是来了这一窝蜂的人,老大爷一次演出只让进一我的,下次演出才让下一我的进,这个就叫debounce

OK, text is long, show you code.

如下code来自underscore.js(相似jQuery的库,封装了一些方法)

// Returns a function, that, when invoked, will only be triggered at most once
  // during a given window of time. Normally, the throttled function will run
  // as much as it can, without ever going more than once per `wait` duration;
  // but if you'd like to disable the execution on the leading edge, pass
  // `{leading: false}`. To disable execution on the trailing edge, ditto.
  _.throttle = function(func, wait, options) {
    var timeout, context, args, result;
    // 标记时间戳
    var previous = 0;
    // options可选属性 leading: true/false 表示第一次事件立刻触发回调/等待wait时间后触发
    // options可选属性 trailing: true/false 表示最后一次回调触发/最后一次回调不触发
    if (!options) options = {};

    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };

    var throttled = function() {
      // 记录当前时间戳
      var now = _.now();
      // 若是是第一次触发且选项设置不当即执行回调
      if (!previous && options.leading === false)
      // 将记录的上次执行的时间戳置为当前
      previous = now;
      // 距离下次触发回调还需等待的时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;

      // 等待时间 <= 0或者不科学地 > wait(异常状况)
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
            // 清除定时器
          clearTimeout(timeout);
          // 解除引用
          timeout = null;
        }
        // 将记录的上次执行的时间戳置为当前
        previous = now;

        // 触发回调
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
      // 在定时器不存在且选项设置最后一次触发须要执行回调的状况下
      // 设置定时器,间隔remaining时间后执行later
      else if (!timeout && options.trailing !== false)    {
        timeout = setTimeout(later, remaining);
      }
     return result;
    };

    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };

    return throttled;
  };
 

// Returns a function, that, as long as it continues to be invoked, will not
  // be triggered. The function will be called after it stops being called for
  // N milliseconds. If `immediate` is passed, trigger the function on the
  // leading edge, instead of the trailing.
  _.debounce = function(func, wait, immediate) {
    var timeout, result;

     // 定时器设置的回调,清除定时器,执行回调函数func
    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

     // restArgs函数将传入的func的参数改形成Rest Parameters —— 一个参数数组
    var debounced = restArgs(function(args) {
      if (timeout) clearTimeout(timeout);
      if (immediate) {
        // 当即触发的条件:immediate为true且timeout为空
        var callNow = !timeout;
        timeout = setTimeout(later, wait);
        if (callNow) result = func.apply(this, args);
      } else {
        // _.delay方法其实是setTimeout()包裹了一层参数处理的逻辑
        timeout = _.delay(later, wait, this, args);
      }

      return result;
    });

    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };

    return debounced;
  };

 

对比以上代码,咱们能够发现,两种方法应用的场景时有差异的

  • 函数节流:适用于屡次提交(commit)的场景,如点击按钮提交发送请求的状况
  • 函数去抖:适用于scroll/resize等场景

相对于屡次触发只执行一次的debounce,间隔地执行回调的throttle更能知足“稀释”scroll事件的需求。

至于wait的设定值,到底多久执行一次比较合适?很大部分仍是取决于具体的场景&代码复杂度,可是这里有一个例子能够参考:Learning from Twitter

2011年Twitter出现过滚动性能差到严重影响用户体验的案例,缘由是

It’s a very, very, bad idea to attach handlers to the window scroll event.

Always cache the selector queries that you’re re-using.

最后采用了函数节流的办法:

var outerPane = $details.find(".details-pane-outer"),
    didScroll = false;

$(window).scroll(function() {
    didScroll = true;
});

setInterval(function() {
    if ( didScroll ) {
        didScroll = false;
        // Check your page position and then
        // Load in more results
    }
}, 250);

 

示例中给出的数字250,能够给你们参考一下~

 

去定时器

为何定时器会引发掉帧?

 

如你所见,定时器致使掉帧的缘由,就在于没法准确控制回调执行的时机。

即便给定时器设置延时时间wait刚好为16.7ms,也不行。

js的单线程限制了回调会在16.7ms以后加入任务队列,却不能保证必定在16.7ms以后触发。若是当下js正在进行耗时计算,回调就只能等着。因此实际上回调执行的时机,是定时器设置后 >= 16.7ms后。

那么去定时器是否意味着否认了以前说的函数去抖和函数节流操做?

NONONO,这两种提高scroll性能的操做应用于不一样的场景:

  • scroll过程当中伴随着不直接改变画面效果的计算操做,如懒加载、loadmore等,在这样的scroll场景里,咱们要不断进行判断操做,大量的计算操做就可能阻塞scroll,因此要对操做进行“稀释”。
  • scroll过程当中伴随着直接改变画面效果的操做,如动画、affix引发的scroll滚动等。

案例:在这个世界上,有一种经典的导航栏形式,那就是,affix。

这种导航栏在你scroll时会粘在你的窗口的固定位置(通常是top),而且在你点击导航栏时自动滚动到页面对应的target内容。

不动戳我

这是我本身作的一个小demo,利用了setInterval,每16.7ms设置scrollTop + 5px,达到“平滑”滚动的效果。

emmmm,看着不规则的锯齿,难受。

若是还不够明显,试试将wait设为50ms

看起来,要遇上每个标准帧渲染的时机,不是那么容易,可是旁友,你据说过安利吗?哦走错片场了,是requestAnimationFrame()requestIdleCallback().

requestAnimationFrame()

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.

能够将它看作一个钩子,恰好卡在浏览器重绘前向咱们的操做伸出橄榄枝。实际上它更像定时器,每秒60次执行回调——符合屏幕的刷新频率,遇到耗时长的操做,这个数字会降到30来保证稳定的帧数。

语法也很简单:window.requestAnimationFrame(callback)

更改后的代码:

const newScrollTop = this.getPosition(this.panes[index].$refs.content).top - this.distance

function scrollStep() {
    document.documentElement.scrollTop += 5
    if (document.documentElement.scrollTop < newScrollTop) {
        window.requestAnimationFrame(scrollStep)
    }
}

window.requestAnimationFrame(scrollStep)

 

与定时器很类似,只是鉴于其一次执行只调用一次回调,因此须要以递归的方式书写。

测试一下:

 

能够说是很顺滑了~

兼容性呢?

Learn more about requestAnimationFrame()

 

requestIdleCallback()

The window.requestIdleCallback() method queues a function to be called during a browser's idle periods. This enables developers to perform background and low priority work on the main event loop, without impacting latency-critical events such as animation and input response. Functions are generally called in first-in-first-out order; however, callbacks which have a timeout specified may be called out-of-order if necessary in order to run them before the timeout elapses.

意思是,它会在一帧末尾浏览器空闲时触发回调,不然,推迟到下一帧。

看定义,它适合应用于执行在后台运行或者优先度低的任务,可是鉴于咱们的案例逻辑和计算都比较简单,应该能知足一帧末尾有空闲(毕竟标题是“不择手段”),have a try.

实际上,基础使用上requestIdleCallback()requestAnimationFrame()语法相同,代码修改甚至也只替换了方法名。

应用状况呢?

 

也是如丝般顺滑~仔细看每一帧,咱们会发现,Fire Idle Callback正如其定义,出如今每帧的最后。

可是兼容性看起来除了chrome和FireFox以外,就不是那么友好了:

总结

在追求高性能的渲染效果时,能够考虑用requestIdleCallback()requestAnimationFrame()代替定时器。前者适合流畅的动画效果场景,后者适用于分离一些优先级低的操做逻辑,使用时须要考虑清楚。

 

避免强制重排

记忆力好的同窗可能还记得,咱们在以前描述浏览器渲染过程时,提到一个强制重排的概念,它的特色是,会插队!

注意红线,意思是可能会在JS里强制重排,当访问scrollWidth系列、clientHeight系列、offsetTop系列、ComputedStyle等属性时,会触发这个效果,致使Style和Layout前移到JS代码执行过程当中

这个强制重排(force layout)听起来好像和重排很像啊,那么它和重排以及重绘是什么关系呢?

优秀的前端工程师对重绘和重绘的概念已经很熟悉了,我这里就再也不赘述。浏览器有本身的优化机制,包括以前提到的每帧只响应同类别的事件一次,再好比这里的会把一帧里的屡次重排、重绘汇总成一次进行处理。

flush队列是浏览器进行重排、重绘等操做的队列,全部会引发重排重绘的操做都包含在内,好比dom修改、样式修改等。若是每次js操做都去执行一次重排重绘,那么浏览器必定会卡卡卡卡卡,因此浏览器一般是在必定的时间间隔(一帧)内,批量处理队列里的操做。可是,对于有些操做,好比获取元素相对父级元素左边界的偏移值(Element.offsetLeft),但在此以前咱们进行了样式或者dom修改,这个操做还攒在flush队列里没有执行,那么浏览器为了让咱们获取正确的offsetLeft(虽然以前的操做可能不会影响offsetLeft的值),就会当即执行队列里的操做。

 

因此咱们知道了,就是这个特殊操做会影响浏览器正常的执行和渲染,假设咱们频繁执行这样的特殊操做,就会打断浏览器原来的节奏,增大开销。

而这个特殊操做,具体指的就是:

  • elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent
  • elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight
  • elem.getClientRects(), elem.getBoundingClientRect()
  • elem.scrollWidth, elem.scrollHeight
  • elem.scrollLeft, elem.scrollTop
  • ...

See more:What forces layout / reflow

解决办法呢,有俩:

  • 基础版:使用前面提到过的requestAnimationFrame(),将以上特殊操做聚集并延迟入队
  • 进阶版:使用第三方FastDom帮助咱们自动完成读写操做的批处理,实际上它也是创建在requestAnimationFrame()上构造的。官方提供的example看起来效果简直优秀
FastDom works as a regulatory layer between your app/library and the DOM. By batching DOM access we avoid unnecessary document reflows and dramatically speed up layout performance.
Each measure/mutate job is added to a corresponding measure/mutate queue. The queues are emptied (reads, then writes) at the turn of the next frame using window.requestAnimationFrame.
FastDom aims to behave like a singleton across all modules in your app. When any module requires 'fastdom' they get the same instance back, meaning FastDom can harmonize DOM access app-wide.
Potentially a third-party library could depend on FastDom, and better integrate within an app that itself uses it.

总结

谨慎使用以上特殊的读操做,要使用也尽可能聚集、包裹(requestAnimationFrame()),避免单个裸奔。

Learn more about how to giagnose forced synchronous layouts with chrome DevTools

 

提高合成层

不知道有没有人,曾经围坐在黑夜里的炉火旁边,听前端前辈们传递智慧的话语 —— 作位移效果时使用tranform代替top/left/bottom/right,尤为是移动端!

why?

由于top/left/bottom/right属性性能差呀 —— 这类属性会影响元素在文档中的布局,可能改变其余元素的位置,引发重排,形成性能开销

由于tranform属性性能好呀 —— 使用transform属性(3D/animation)将元素提高至合成层,省去布局和绘制环节,美滋滋~

说到这里,你可能还不是太清楚合成层的概念,其实看这篇就够了:无线性能优化:Composite

可是照顾一下有些“太长不看”猫病的旁友们,在这里作一些总结。

1.一些属性会让元素们建立出不一样的渲染层

  • 有明确的定位属性(relative、fixed、sticky、absolute)
  • 透明的(opacity 小于 1)
  • 有 CSS 滤镜(fliter)
  • 有 CSS transform 属性(不为 none)
  • ...

2.达成一些条件,渲染层会提高为合成层

  • 硬件加速的 iframe 元素(好比 iframe 嵌入的页面中有合成层)
  • 3D 或者 硬件加速的 2D Canvas 元素
  • video 元素
  • 有 3D transform
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等须要设置明确的定位属性,如 relative 等)
  • ...

提高为合成层干什么呢?普通的渲染层普通地渲染,用普通的顺序普通地合成很差吗?非要搞啥特殊待遇!

浏览器就说了:我这也是为了你们共同进步(提高速度)!看那些搞特殊待遇的,都是一些拖咱们队伍后腿的(性能开销大),分开处理,才能保证整个队伍稳定快速的进步!

特殊待遇:合成层的位图,会交由 GPU 合成,比 CPU 处理要快。当须要 repaint 时,只须要 repaint 自己,不会影响到其余的层。

对布局属性进行动画,浏览器须要为每一帧进行重绘并上传到 GPU 中

对合成属性进行动画,浏览器会为元素建立一个独立的复合层,当元素内容没有发生改变,该层就不会被重绘,浏览器会经过从新复合来建立动画帧

因此,从合成层出发,为了优化scroll性能,咱们能够作这些:

will-change

提高合成层的有效方式,应用这个属性,其实是提早通知浏览器,为接下来的动画效果操做作准备。值得注意的是

  • 不要将 will-change 应用到太多元素上,增长渲染层意味着新的内存分配和更复杂的层的管理
  • 有节制地使用。动态样式增长比一开始就写在样式表里更能减小没必要要的开销。

示例:

will-change: scroll-position // 表示开发者但愿在不久后改变滚动条的位置或者使之产生动画

而后,国际惯例【并不,附上兼容性

 

 

除此以外

  • 使用 transform 或者 opacity 来实现动画效果
  • 对于较少可能变化的区域,防止页面其余部分重绘时影响这一片,考虑提高至合成层。
  • 提高合成层的hack方法:translateZ(0)

总结

从合成层的角度做为性能提高的下手方向,是值得确定的,可是具体采用什么样的方案,仍是要先切实地分析页面的实际性能表现,根据不一样的场景,综合考虑方案的得失,再总结出正确的优化途径。

 

what's more

使用css属性代替js“模拟操做”

scroll-behavior

The scroll-behavior CSS property specifies the scrolling behavior for a scrolling box, when scrolling happens due to navigation or CSSOM scrolling APIs. Any other scrolls, e.g. those that are performed by the user, are not affected by this property. When this property is specified on the root element, it applies to the viewport instead.

能够借此实现affix,而不用使用定时器或requestAnimationFrame模拟平滑的scroll操做

相关文章
相关标签/搜索