[轉(zhuǎn)載]如何不擇手段提升scroll事件的性能

原文地址

TL;DR

1. chrome devtool 是診斷頁(yè)面滾動(dòng)性能的有效工具

2. 提升滾動(dòng)時(shí)性能池凄,就是要達(dá)到fps高且穩(wěn)。

3. 具體可以從以下方面著手

  • 使用web worker分離無(wú)頁(yè)面渲染無(wú)關(guān)的邏輯計(jì)算
  • 觸發(fā)監(jiān)聽(tīng)事件時(shí)使用函數(shù)節(jié)流與函數(shù)去抖
  • 使用requestAnimationFrame與requestIdleCallback代替定時(shí)器
  • 避免強(qiáng)制重排
  • 提升合成層

場(chǎng)景

滾動(dòng)行為無(wú)時(shí)無(wú)刻不出現(xiàn)在我們?yōu)g覽網(wǎng)頁(yè)的行為中,在許多場(chǎng)景中致稀,我們有有意識(shí)地宗苍、主動(dòng)地去使用滾動(dòng)操作,比如:

  • 懶加載
  • loadmore
  • affix
  • 回到頂部

以上場(chǎng)景伴隨著滾動(dòng)事件的監(jiān)聽(tīng)操作佃却,一不留神可能就讓頁(yè)面的滾動(dòng)不再“如絲般順滑”者吁。

不擇手段打造一個(gè)卡頓的scroll場(chǎng)景:

作為一名優(yōu)秀的前端工程師(未來(lái)的),怎么能容許出現(xiàn)這種情況饲帅!不就性能優(yōu)化嗎复凳,撩起袖子就是干瘤泪!


原理

在一個(gè)流暢的頁(yè)面變化效果中(動(dòng)畫(huà)或滾動(dòng)),渲染幀育八,指的是瀏覽器從js執(zhí)行到paint的一次繪制過(guò)程对途,幀與幀之間快速地切換,由于人眼的殘像錯(cuò)覺(jué)髓棋,就形成了動(dòng)畫(huà)的效果实檀。那么這個(gè)“快速”,要達(dá)到多少才合適呢仲锄?

我們都知道劲妙,下層建筑決定了上層建筑。受限于目前大多數(shù)屏幕的刷新頻率——60次/s儒喊,瀏覽器的渲染更新的頁(yè)面的標(biāo)準(zhǔn)幀率也為60次/s--60FPS(frames/per second)镣奋。

  • 高于這個(gè)數(shù)字,在一次屏幕刷新的時(shí)間間隔16.7ms(1/60)內(nèi)怀愧,就算瀏覽器渲染了多次頁(yè)面侨颈,屏幕也只刷新一次,這就造成了性能的浪費(fèi)芯义。
  • 低于這個(gè)數(shù)字哈垢,幀率下降,人眼就可能捕捉到兩幀之間變化的滯澀與突兀扛拨,表現(xiàn)在屏幕上耘分,就是頁(yè)面的抖動(dòng),大家通常稱(chēng)之為卡頓

來(lái)個(gè)比喻绑警∏筇快遞每天整理包裹,并一天一送计盒。如果某天包裹太多渴频,整理花費(fèi)了太多時(shí)間,來(lái)不及當(dāng)日(幀)送到收件人處北启,那就延期了(丟幀)卜朗。

那么在這16.7ms之內(nèi),瀏覽器都干了什么呢咕村?

瀏覽器內(nèi)心OS:不要老抱怨我延期(丟幀)场钉,我也很忙的好伐?

幀維度解釋幀渲染過(guò)程

瀏覽器渲染頁(yè)面的Renderer進(jìn)程里懈涛,涉及到了兩個(gè)線程惹悄,二者之間通過(guò)名為Commit的消息保持同步:

  • Main線程:瀏覽器渲染的主要執(zhí)行步驟,包含從JS執(zhí)行到Composite合成的一系列操作(下文會(huì)介紹)
  • Compositor線程:接收用戶的一些交互操作(比如滾動(dòng)) => 喚起Main線程進(jìn)行操作 => 接收Main線程的操作結(jié)果 => commit給真正把頁(yè)面draw到屏幕上的GPU進(jìn)程

標(biāo)準(zhǔn)渲染幀:

在一個(gè)標(biāo)準(zhǔn)幀渲染時(shí)間16.7ms之內(nèi)肩钠,瀏覽器需要完成Main線程的操作泣港,并commit給Compositor進(jìn)程

丟幀:

主線程里操作太多暂殖,耗時(shí)長(zhǎng),commit的時(shí)間被推遲当纱,瀏覽器來(lái)不及將頁(yè)面draw到屏幕微姊,這就丟失了一幀

那么Main線程里都有些什么操作會(huì)導(dǎo)致延時(shí)呢仓技?

進(jìn)一步解釋瀏覽器主要執(zhí)行步驟

  • JavaScript:包含與視覺(jué)變化效果相關(guān)的js操作。包括并不限于:dom更新、元素樣式動(dòng)態(tài)改變寥院、jQuery的animate函數(shù)等苞冯。
  • Style:樣式計(jì)算镀脂。這個(gè)過(guò)程帐姻,瀏覽器根據(jù)css選擇器計(jì)算哪些元素應(yīng)該應(yīng)用哪些規(guī)則,然后將樣式規(guī)則落實(shí)到每個(gè)元素上去悯恍,確定每個(gè)元素具體的樣式库糠。
  • Layout:布局。在知道對(duì)一個(gè)元素應(yīng)用哪些規(guī)則之后涮毫,瀏覽器即可開(kāi)始計(jì)算它要占據(jù)的空間大小及其在屏幕的位置瞬欧。
  • Painting:繪制。繪制是填充像素的過(guò)程罢防。它涉及繪出文本艘虎、顏色、圖像咒吐、邊框和陰影野建,基本上包括元素的每個(gè)可視部分。繪制一般是在多個(gè)表面(通常稱(chēng)為層)上完成的恬叹。(paint和draw的區(qū)別:paint是把內(nèi)容填充到頁(yè)面候生,而draw是把頁(yè)面反映到屏幕上)
  • Composite:合成。由于頁(yè)面的各部分可能被繪制到多層妄呕,由此它們需要按正確順序繪制到屏幕上陶舞,以便正確渲染頁(yè)面嗽测。對(duì)于與另一元素重疊的元素來(lái)說(shuō)绪励,這點(diǎn)特別重要,因?yàn)橐粋€(gè)錯(cuò)誤可能使一個(gè)元素錯(cuò)誤地出現(xiàn)在另一個(gè)元素的上層唠粥。

理論上疏魏,每次標(biāo)準(zhǔn)的渲染,瀏覽器Main線程需要執(zhí)行JavaScript => Style => Layout => Paint => Composite五個(gè)步驟晤愧,但是實(shí)際上大莫,要分場(chǎng)景。

指路官網(wǎng)

再進(jìn)一步解釋瀏覽器渲染流程

流程:

1.Compositor線程接收一個(gè)vsync信號(hào)官份,表示這一幀開(kāi)始

2.Compositor線程接收用戶的交互輸入(比如touchmove只厘、scroll烙丛、click等)。然后commit給Main線程羔味,這里有兩點(diǎn)規(guī)則需要注意:

  • 并不是所有event都會(huì)commit給Main線程河咽,部分操作比如單純的滾動(dòng)事件,打字等輸入赋元,不需要執(zhí)行JS忘蟹,也沒(méi)有需要重繪的場(chǎng)景,Compositor線程就自己處理了搁凸,無(wú)需請(qǐng)求Main線程
  • 同樣的事件類(lèi)型媚值,不論一幀內(nèi)被Compositor線程接收多少次,實(shí)際上commit給Main線程的护糖,只會(huì)是一次褥芒,意味著也只會(huì)被執(zhí)行一次。(HTML5標(biāo)準(zhǔn)里scroll事件是每幀觸發(fā)一次)

3.Main線程執(zhí)行從JavaScript到Composite的過(guò)程椅文,也有兩點(diǎn)需要注意:

  • 注意紅線喂很,意思是可能會(huì)在JS里強(qiáng)制重排,當(dāng)訪問(wèn)scrollWidth系列皆刺、clientHeight系列少辣、offsetTop系列、ComputedStyle等屬性時(shí)羡蛾,會(huì)觸發(fā)這個(gè)效果漓帅,導(dǎo)致Style和Layout前移到JS代碼執(zhí)行過(guò)程中。
  • 實(shí)際上圖中省略了Renderer進(jìn)程中的其他線程痴怨,比如當(dāng)Main線程走到j(luò)s執(zhí)行這一步時(shí)忙干,會(huì)調(diào)起單獨(dú)的js線程來(lái)執(zhí)行。另外還有如HTML解釋線程等浪藻。

4.當(dāng)Main線程完成最后合成之后捐迫,與Compositor線程使用commit進(jìn)行通信,Compositor調(diào)起Compositor Tile Work(s)來(lái)輔助處理頁(yè)面爱葵。Rasterize意為光柵化施戴,想深入了解什么是光柵的小伙伴可以戳這里了解:瀏覽器渲染詳細(xì)過(guò)程:重繪、重排和composite只是冰山一角

5.頁(yè)面paint結(jié)束之后萌丈,這一幀就結(jié)束了赞哗。GPU進(jìn)程里的GPU線程負(fù)責(zé)把Renderer進(jìn)程操作好的頁(yè)面,交由GPU辆雾,調(diào)用GPU內(nèi)方法肪笋,由GPU把頁(yè)面draw到屏幕上。

6.屏幕刷新,我們就在瀏覽器(屏幕)上看到了新頁(yè)面藤乙。

接下來(lái)猜揪,簡(jiǎn)要介紹一下,如何使用chrome devtool分析頁(yè)面性能坛梁。

示意圖(chrome version: 61):

  • 幀率概覽湿右。看頂端綠色長(zhǎng)條罚勾,越高代表幀率越高毅人,高低起伏多代表幀率變化不穩(wěn)定,越坑坑洼洼代表容易產(chǎn)生視覺(jué)上的卡頓尖殃。
  • 分析具體某一幀丈莺。如果發(fā)現(xiàn),有哪一幀幀率特別低送丰,可以在中間那一欄找到耗時(shí)長(zhǎng)的那一幀缔俄,點(diǎn)擊進(jìn)行具體的活動(dòng)分析。
  • 分析個(gè)活動(dòng)耗時(shí)器躏。自由選擇某一段或某一幀觀察這段時(shí)間內(nèi)各項(xiàng)活動(dòng)的耗時(shí)來(lái)診斷頁(yè)面俐载。(注意顏色)

應(yīng)該注意,我們可以看見(jiàn)登失,很少有幀的時(shí)間準(zhǔn)確卡在了16.7s遏佣,實(shí)際上每幀達(dá)到60fps的幀率,只是一個(gè)理想化的數(shù)字揽浙,瀏覽器執(zhí)行過(guò)程中可能受到各種情況的干擾状婶。而我們?nèi)搜垡矝](méi)有那么靈敏,只要達(dá)到20幀以上馅巷,頁(yè)面看起來(lái)就比較流暢了膛虫。尤其是結(jié)構(gòu)復(fù)雜,數(shù)據(jù)較多的頁(yè)面钓猬,盲目追求60fps只是鉆牛角尖稍刀。所以,以我淺見(jiàn)敞曹,穩(wěn)定的fps更能影響scroll效果账月。

關(guān)于更加具體地如何使用chome devtool分析頁(yè)面性能,戳:Performance Analysis Reference


解決方案

我們的目標(biāo)很明確异雁,就是拒絕卡頓捶障!具體說(shuō)來(lái)就是盡量趕在16.7ms之內(nèi)讓瀏覽器完成五項(xiàng)工作僧须,壓縮每個(gè)步驟時(shí)間纲刀。

使用web worker

當(dāng)我們了解了瀏覽器渲染時(shí)執(zhí)行的過(guò)程,并且清楚瀏覽器內(nèi)核處理方式(處理js的線程與GUI頁(yè)面渲染線程互斥)之后,我們很容易假想出這樣一種狀況:如果js大量的計(jì)算和邏輯操作霸占著瀏覽器示绊,使頁(yè)面渲染得不到處理锭部,怎么辦?

這種情況面褐,很容易造成scroll的卡頓拌禾,甚至瀏覽器假死,就像alert()出現(xiàn)一樣展哭。

想象一下吧湃窍,本來(lái)大家好好地按照生理周期一個(gè)接一個(gè)上廁所,突然小j便秘了匪傍!你說(shuō)排在他后面的小g急不急您市,可急死了!

web worker是什么役衡?

Web Worker為Web內(nèi)容在后臺(tái)線程中運(yùn)行腳本提供了一種簡(jiǎn)單的方法茵休。線程可以執(zhí)行任務(wù)而不干擾用戶界面。

這就好像手蝎,給容易“便秘”的小j榕莺,單獨(dú)搭了個(gè)簡(jiǎn)易廁所。

之所以說(shuō)這是一個(gè)簡(jiǎn)易廁所棵介,因?yàn)樗幸恍┫拗?/p>

  • 無(wú)法訪問(wèn)DOM節(jié)點(diǎn)
  • 無(wú)法訪問(wèn)全局變量或是全局函數(shù)
  • 無(wú)法調(diào)用alert()或者confirm之類(lèi)的函數(shù)
  • 無(wú)法訪問(wèn)window钉鸯、document之類(lèi)的瀏覽器全局變量

主線程和 worker 線程之間通過(guò)這樣的方式互相傳輸信息:兩端都使用 postMessage() 方法來(lái)發(fā)送信息, 并且通過(guò) onmessage 這個(gè) event handler來(lái)接收信息。 (傳遞的信息包含在 Message 這個(gè)事件的數(shù)據(jù)屬性內(nèi)) 邮辽。數(shù)據(jù)的交互是通過(guò)傳遞副本亏拉,而不是直接共享數(shù)據(jù)。

使用案例 - 判斷素?cái)?shù)

案例來(lái)自Web Workers, for a responsive JavaScript application

素?cái)?shù)逆巍,定義為在大于1的自然數(shù)中及塘,除了1和它本身以外不再有其他因數(shù)。判斷算法為锐极,以2到它的平方根為界取整數(shù)做循環(huán)判斷笙僚,用它和這個(gè)數(shù)字求余數(shù),只要中間任意一次計(jì)算得到余數(shù)為零灵再,則能夠確認(rèn)這個(gè)數(shù)字不是質(zhì)數(shù)肋层。

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);
};

代碼說(shuō)明:

  • 使用web worker對(duì)一個(gè)較大數(shù)字(1000001111111111)進(jìn)行素?cái)?shù)判斷
  • 得到結(jié)果之后alert(number + ' is prime. Now I'll try calculating without a web worker.')
  • 在不使用web worker的情況下,對(duì)相同數(shù)字進(jìn)行素?cái)?shù)判斷翎迁,完成后alert('I am sure, it is prime. ')
  • 從頁(yè)面標(biāo)簽里的內(nèi)容的滾動(dòng)情況判斷兩次計(jì)算對(duì)瀏覽器/頁(yè)面造成的影響

現(xiàn)場(chǎng)還原:


不動(dòng)戳我

案例總結(jié)
從兩次alert之后的段落滾動(dòng)情況(第二次根本動(dòng)不了)栋猖,足以看出大量繁雜的js計(jì)算對(duì)頁(yè)面的影響。恰當(dāng)?shù)厥褂脀eb worker汪榔,能有效緩解頁(yè)面scroll阻塞的情況蒲拉。

而且它的支持率也良好~


在應(yīng)用方面,Angular已經(jīng)做了一些嘗試。

解密Angular WebWorker Renderer (一):想辦法打破web worker本身不能操作dom元素等限制雌团,利用web worker執(zhí)行渲染操作

Learn more about web worker

函數(shù)節(jié)流與函數(shù)去抖

針對(duì)scroll事件中的回調(diào)燃领,思路之一是對(duì)事件進(jìn)行“稀釋”,減少事件回調(diào)的執(zhí)行次數(shù)锦援。

這就涉及到兩個(gè)概念:函數(shù)節(jié)流和函數(shù)去抖

  • 函數(shù)節(jié)流(throttle):讓函數(shù)在指定的時(shí)間段內(nèi)周期性地間斷執(zhí)行
  • 函數(shù)去抖(debounce):讓函數(shù)只有在過(guò)完一段時(shí)間后并且該段時(shí)間內(nèi)不被調(diào)用才會(huì)被執(zhí)行

有人這樣比喻:

就像一窩蜂的人去排隊(duì)看演出猛蔽,隊(duì)伍很亂,看門(mén)的老大爺每隔1秒灵寺,讓進(jìn)一個(gè)人曼库,這個(gè)叫throttle,如果來(lái)了這一窩蜂的人略板,老大爺一次演出只讓進(jìn)一個(gè)人凉泄,下次演出才讓下一個(gè)人進(jìn),這個(gè)就叫debounce

OK, text is long, show you code.

以下code來(lái)自underscore.js(類(lèi)似jQuery的庫(kù)蚯根,封裝了一些方法)

// 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;
    // 標(biāo)記時(shí)間戳
    var previous = 0;
    // options可選屬性 leading: true/false 表示第一次事件馬上觸發(fā)回調(diào)/等待wait時(shí)間后觸發(fā)
    // options可選屬性 trailing: true/false 表示最后一次回調(diào)觸發(fā)/最后一次回調(diào)不觸發(fā)
    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() {
      // 記錄當(dāng)前時(shí)間戳
      var now = _.now();
      // 如果是第一次觸發(fā)且選項(xiàng)設(shè)置不立即執(zhí)行回調(diào)
      if (!previous && options.leading === false)
      // 將記錄的上次執(zhí)行的時(shí)間戳置為當(dāng)前
      previous = now;
      // 距離下次觸發(fā)回調(diào)還需等待的時(shí)間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;

      // 等待時(shí)間 <= 0或者不科學(xué)地 > wait(異常情況)
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
            // 清除定時(shí)器
          clearTimeout(timeout);
          // 解除引用
          timeout = null;
        }
        // 將記錄的上次執(zhí)行的時(shí)間戳置為當(dāng)前
        previous = now;

        // 觸發(fā)回調(diào)
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      }
      // 在定時(shí)器不存在且選項(xiàng)設(shè)置最后一次觸發(fā)需要執(zhí)行回調(diào)的情況下
      // 設(shè)置定時(shí)器后众,間隔remaining時(shí)間后執(zhí)行l(wèi)ater
      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;

     // 定時(shí)器設(shè)置的回調(diào),清除定時(shí)器颅拦,執(zhí)行回調(diào)函數(shù)func
    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

     // restArgs函數(shù)將傳入的func的參數(shù)改造成Rest Parameters —— 一個(gè)參數(shù)數(shù)組
    var debounced = restArgs(function(args) {
      if (timeout) clearTimeout(timeout);
      if (immediate) {
        // 立即觸發(fā)的條件:immediate為true且timeout為空
        var callNow = !timeout;
        timeout = setTimeout(later, wait);
        if (callNow) result = func.apply(this, args);
      } else {
        // _.delay方法實(shí)際上是setTimeout()包裹了一層參數(shù)處理的邏輯
        timeout = _.delay(later, wait, this, args);
      }

      return result;
    });

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

    return debounced;
  };

對(duì)比以上代碼蒂誉,我們可以發(fā)現(xiàn),兩種方法應(yīng)用的場(chǎng)景時(shí)有差別的

  • 函數(shù)節(jié)流:適用于多次提交(commit)的場(chǎng)景距帅,如點(diǎn)擊按鈕提交發(fā)送請(qǐng)求的情況
  • 函數(shù)去抖:適用于scroll/resize等場(chǎng)景

相對(duì)于多次觸發(fā)只執(zhí)行一次的debounce右锨,間隔地執(zhí)行回調(diào)的throttle更能滿足“稀釋”scroll事件的需求。

至于wait的設(shè)定值碌秸,到底多久執(zhí)行一次比較合適绍移?很大部分還是取決于具體的場(chǎng)景&代碼復(fù)雜度,但是這里有一個(gè)例子可以參考:Learning from Twitter

2011年Twitter出現(xiàn)過(guò)滾動(dòng)性能差到嚴(yán)重影響用戶體驗(yàn)的案例讥电,原因是

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.

最后采用了函數(shù)節(jié)流的辦法:

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);

示例中給出的數(shù)字250蹂窖,可以給大家參考一下~

去定時(shí)器

為什么定時(shí)器會(huì)引起掉幀?


如你所見(jiàn)恩敌,定時(shí)器導(dǎo)致掉幀的原因瞬测,就在于無(wú)法準(zhǔn)確控制回調(diào)執(zhí)行的時(shí)機(jī)。

即使給定時(shí)器設(shè)置延時(shí)時(shí)間wait恰好為16.7ms纠炮,也不行月趟。

js的單線程限制了回調(diào)會(huì)在16.7ms之后加入任務(wù)隊(duì)列,卻不能保證一定在16.7ms之后觸發(fā)恢口。如果當(dāng)下js正在進(jìn)行耗時(shí)計(jì)算孝宗,回調(diào)就只能等著。所以實(shí)際上回調(diào)執(zhí)行的時(shí)機(jī)耕肩,是定時(shí)器設(shè)置后 >= 16.7ms后因妇。

那么去定時(shí)器是否意味著否定了之前說(shuō)的函數(shù)去抖和函數(shù)節(jié)流操作问潭?

NONONO,這兩種提升scroll性能的操作應(yīng)用于不同的場(chǎng)景:

  • scroll過(guò)程中伴隨著不直接改變畫(huà)面效果的計(jì)算操作沙峻,如懶加載、loadmore等两芳,在這樣的scroll場(chǎng)景里摔寨,我們要不斷進(jìn)行判斷操作,大量的計(jì)算操作就可能阻塞scroll怖辆,所以要對(duì)操作進(jìn)行“稀釋”是复。
  • scroll過(guò)程中伴隨著直接改變畫(huà)面效果的操作,如動(dòng)畫(huà)竖螃、affix引起的scroll滾動(dòng)等淑廊。

案例:在這個(gè)世界上,有一種經(jīng)典的導(dǎo)航欄形式特咆,那就是季惩,affix。

這種導(dǎo)航欄在你scroll時(shí)會(huì)粘在你的窗口的固定位置(一般是top)腻格,并且在你點(diǎn)擊導(dǎo)航欄時(shí)自動(dòng)滾動(dòng)到頁(yè)面對(duì)應(yīng)的target內(nèi)容画拾。

不動(dòng)戳我

這是我自己做的一個(gè)小demo,利用了setInterval菜职,每16.7ms設(shè)置scrollTop + 5px青抛,達(dá)到“平滑”滾動(dòng)的效果。

emmmm酬核,看著不規(guī)則的鋸齒蜜另,難受。

如果還不夠明顯嫡意,試試將wait設(shè)為50ms


看起來(lái)举瑰,要趕上每一個(gè)標(biāo)準(zhǔn)幀渲染的時(shí)機(jī),不是那么容易蔬螟,但是旁友嘶居,你聽(tīng)說(shuō)過(guò)安利嗎?哦走錯(cuò)片場(chǎng)了促煮,是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.

可以將它看做一個(gè)鉤子邮屁,剛好卡在瀏覽器重繪前向我們的操作伸出橄欖枝。實(shí)際上它更像定時(shí)器菠齿,每秒60次執(zhí)行回調(diào)——符合屏幕的刷新頻率佑吝,遇到耗時(shí)長(zhǎng)的操作,這個(gè)數(shù)字會(huì)降到30來(lái)保證穩(wěn)定的幀數(shù)绳匀。

語(yǔ)法也很簡(jiǎn)單: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)

與定時(shí)器很相似芋忿,只是鑒于其一次執(zhí)行只調(diào)用一次回調(diào)炸客,所以需要以遞歸的方式書(shū)寫(xiě)。

測(cè)試一下:


可以說(shuō)是很順滑了~

兼容性呢戈钢?


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.

意思是痹仙,它會(huì)在一幀末尾瀏覽器空閑時(shí)觸發(fā)回調(diào),否則殉了,推遲到下一幀开仰。

看定義,它適合應(yīng)用于執(zhí)行在后臺(tái)運(yùn)行或者優(yōu)先度低的任務(wù)薪铜,但是鑒于我們的案例邏輯和計(jì)算都比較簡(jiǎn)單众弓,應(yīng)該能滿足一幀末尾有空閑(畢竟標(biāo)題是“不擇手段”),have a try.

實(shí)際上隔箍,基礎(chǔ)使用上requestIdleCallback()requestAnimationFrame()語(yǔ)法相同谓娃,代碼修改甚至也只替換了方法名。

應(yīng)用情況呢蜒滩?


也是如絲般順滑~仔細(xì)看每一幀滨达,我們會(huì)發(fā)現(xiàn),F(xiàn)ire Idle Callback正如其定義俯艰,出現(xiàn)在每幀的最后弦悉。

但是兼容性看起來(lái)除了chrome和FireFox之外,就不是那么友好了:


總結(jié)

在追求高性能的渲染效果時(shí)蟆炊,可以考慮用requestIdleCallback()requestAnimationFrame()代替定時(shí)器稽莉。前者適合流暢的動(dòng)畫(huà)效果場(chǎng)景,后者適用于分離一些優(yōu)先級(jí)低的操作邏輯涩搓,使用時(shí)需要考慮清楚污秆。

避免強(qiáng)制重排

記憶力好的同學(xué)可能還記得,我們?cè)谥懊枋鰹g覽器渲染過(guò)程時(shí)昧甘,提到一個(gè)強(qiáng)制重排的概念良拼,它的特點(diǎn)是,會(huì)插隊(duì)充边!

注意紅線庸推,意思是可能會(huì)在JS里強(qiáng)制重排,當(dāng)訪問(wèn)scrollWidth系列浇冰、clientHeight系列贬媒、offsetTop系列、ComputedStyle等屬性時(shí)肘习,會(huì)觸發(fā)這個(gè)效果际乘,導(dǎo)致Style和Layout前移到JS代碼執(zhí)行過(guò)程中

這個(gè)強(qiáng)制重排(force layout)聽(tīng)起來(lái)好像和重排很像啊,那么它和重排以及重繪是什么關(guān)系呢漂佩?

優(yōu)秀的前端工程師對(duì)重繪和重繪的概念已經(jīng)很熟悉了脖含,我這里就不再贅述罪塔。瀏覽器有自己的優(yōu)化機(jī)制,包括之前提到的每幀只響應(yīng)同類(lèi)別的事件一次养葵,再比如這里的會(huì)把一幀里的多次重排征堪、重繪匯總成一次進(jìn)行處理。

flush隊(duì)列是瀏覽器進(jìn)行重排关拒、重繪等操作的隊(duì)列佃蚜,所有會(huì)引起重排重繪的操作都包含在內(nèi),比如dom修改夏醉、樣式修改等爽锥。如果每次js操作都去執(zhí)行一次重排重繪涌韩,那么瀏覽器一定會(huì)卡卡卡卡卡畔柔,所以瀏覽器通常是在一定的時(shí)間間隔(一幀)內(nèi),批量處理隊(duì)列里的操作臣樱。但是靶擦,對(duì)于有些操作,比如獲取元素相對(duì)父級(jí)元素左邊界的偏移值(Element.offsetLeft)雇毫,但在此之前我們進(jìn)行了樣式或者dom修改玄捕,這個(gè)操作還攢在flush隊(duì)列里沒(méi)有執(zhí)行,那么瀏覽器為了讓我們獲取正確的offsetLeft(雖然之前的操作可能不會(huì)影響offsetLeft的值)棚放,就會(huì)立即執(zhí)行隊(duì)列里的操作枚粘。

所以我們知道了,就是這個(gè)特殊操作會(huì)影響瀏覽器正常的執(zhí)行和渲染飘蚯,假設(shè)我們頻繁執(zhí)行這樣的特殊操作馍迄,就會(huì)打斷瀏覽器原來(lái)的節(jié)奏,增大開(kāi)銷(xiāo)局骤。

而這個(gè)特殊操作攀圈,具體指的就是:

  • 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

解決辦法呢,有倆:

  • 基礎(chǔ)版:使用前面提到過(guò)的requestAnimationFrame()峦甩,將以上特殊操作匯集并延遲入隊(duì)
  • 進(jìn)階版:使用第三方FastDom幫助我們自動(dòng)完成讀寫(xiě)操作的批處理,實(shí)際上它也是建立在requestAnimationFrame()上構(gòu)造的赘来。官方提供的example看起來(lái)效果簡(jiǎn)直優(yōu)秀

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.

總結(jié)

謹(jǐn)慎使用以上特殊的讀操作,要使用也盡量匯集凯傲、包裹(requestAnimationFrame())犬辰,避免單個(gè)裸奔。

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

提升合成層

不知道有沒(méi)有人冰单,曾經(jīng)圍坐在黑夜里的爐火旁邊忧风,聽(tīng)前端前輩們傳遞智慧的話語(yǔ) —— 做位移效果時(shí)使用tranform代替top/left/bottom/right,尤其是移動(dòng)端球凰!

why狮腿?

因?yàn)閠op/left/bottom/right屬性性能差呀 —— 這類(lèi)屬性會(huì)影響元素在文檔中的布局腿宰,可能改變其他元素的位置,引起重排缘厢,造成性能開(kāi)銷(xiāo)

因?yàn)閠ranform屬性性能好呀 —— 使用transform屬性(3D/animation)將元素提升至合成層吃度,省去布局和繪制環(huán)節(jié),美滋滋~

說(shuō)到這里贴硫,你可能還不是太清楚合成層的概念椿每,其實(shí)看這篇就夠了:無(wú)線性能優(yōu)化:Composite

但是照顧一下有些“太長(zhǎng)不看”貓病的旁友們,在這里做一些總結(jié)英遭。

1.一些屬性會(huì)讓元素們創(chuàng)建出不同的渲染層

  • 有明確的定位屬性(relative间护、fixed、sticky挖诸、absolute)
  • 透明的(opacity 小于 1)
  • 有 CSS 濾鏡(fliter)
  • 有 CSS transform 屬性(不為 none)
  • ...

2.達(dá)成一些條件汁尺,渲染層會(huì)提升為合成層

  • 硬件加速的 iframe 元素(比如 iframe 嵌入的頁(yè)面中有合成層)
  • 3D 或者 硬件加速的 2D Canvas 元素
  • video 元素
  • 有 3D transform
  • 對(duì) opacity、transform多律、fliter痴突、backdropfilter 應(yīng)用了 animation 或者 transition
  • will-change 設(shè)置為 opacity、transform狼荞、top辽装、left、bottom相味、right(其中 top拾积、left 等需要設(shè)置明確的定位屬性,如 relative 等)
  • ...

提升為合成層干什么呢丰涉?普通的渲染層普通地渲染拓巧,用普通的順序普通地合成不好嗎?非要搞啥特殊待遇昔搂!

瀏覽器就說(shuō)了:我這也是為了大家共同進(jìn)步(提升速度)玲销!看那些搞特殊待遇的,都是一些拖我們隊(duì)伍后腿的(性能開(kāi)銷(xiāo)大)摘符,分開(kāi)處理贤斜,才能保證整個(gè)隊(duì)伍穩(wěn)定快速的進(jìn)步!

特殊待遇:合成層的位圖逛裤,會(huì)交由 GPU 合成瘩绒,比 CPU 處理要快。當(dāng)需要 repaint 時(shí)带族,只需要 repaint 本身锁荔,不會(huì)影響到其他的層。

對(duì)布局屬性進(jìn)行動(dòng)畫(huà)蝙砌,瀏覽器需要為每一幀進(jìn)行重繪并上傳到 GPU 中

對(duì)合成屬性進(jìn)行動(dòng)畫(huà)阳堕,瀏覽器會(huì)為元素創(chuàng)建一個(gè)獨(dú)立的復(fù)合層跋理,當(dāng)元素內(nèi)容沒(méi)有發(fā)生改變,該層就不會(huì)被重繪恬总,瀏覽器會(huì)通過(guò)重新復(fù)合來(lái)創(chuàng)建動(dòng)畫(huà)幀

所以前普,從合成層出發(fā),為了優(yōu)化scroll性能壹堰,我們可以做這些:

will-change

提升合成層的有效方式拭卿,應(yīng)用這個(gè)屬性,實(shí)際上是提前通知瀏覽器贱纠,為接下來(lái)的動(dòng)畫(huà)效果操作做準(zhǔn)備峻厚。值得注意的是

  • 不要將 will-change 應(yīng)用到太多元素上,增加渲染層意味著新的內(nèi)存分配和更復(fù)雜的層的管理
  • 有節(jié)制地使用谆焊。動(dòng)態(tài)樣式增加比一開(kāi)始就寫(xiě)在樣式表里更能減少不必要的開(kāi)銷(xiāo)惠桃。

示例:

will-change: scroll-position // 表示開(kāi)發(fā)者希望在不久后改變滾動(dòng)條的位置或者使之產(chǎn)生動(dòng)畫(huà)。

然后懊渡,國(guó)際慣例【并不刽射,附上兼容性

除此之外

  • 使用 transform 或者 opacity 來(lái)實(shí)現(xiàn)動(dòng)畫(huà)效果
  • 對(duì)于較少可能變化的區(qū)域军拟,防止頁(yè)面其他部分重繪時(shí)影響這一片剃执,考慮提升至合成層。
  • 提升合成層的hack方法:translateZ(0)

總結(jié)

從合成層的角度作為性能提升的下手方向懈息,是值得肯定的肾档,但是具體采用什么樣的方案,還是要先切實(shí)地分析頁(yè)面的實(shí)際性能表現(xiàn)辫继,根據(jù)不同的場(chǎng)景怒见,綜合考慮方案的得失,再總結(jié)出正確的優(yōu)化途徑姑宽。

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.

可以借此實(shí)現(xiàn)affix遣耍,而不用使用定時(shí)器或requestAnimationFrame模擬平滑的scroll操作

demo戳:錨點(diǎn)鏈接+scroll-behavior

image

但是目前僅是實(shí)驗(yàn)性的功能,殘念



總結(jié)

頁(yè)面渲染性能的優(yōu)化涉及方方面面炮车,這里只是以scroll事件為立足點(diǎn)分析列舉了一些改善的方法舵变,深入性和全面性都不足,但更多希望能起到一個(gè)引子的作用瘦穆,給有心深入的同學(xué)一個(gè)概括性的印象纪隙。


參考鏈接

以下這些大大們的文章都很值得閱讀分析做筆記!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末绵咱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子熙兔,更是在濱河造成了極大的恐慌悲伶,老刑警劉巖艾恼,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異麸锉,居然都是意外死亡蒂萎,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)淮椰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)五慈,“玉大人,你說(shuō)我怎么就攤上這事主穗⌒豪梗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵忽媒,是天一觀的道長(zhǎng)争拐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)晦雨,這世上最難降的妖魔是什么架曹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮闹瞧,結(jié)果婚禮上绑雄,老公的妹妹穿的比我還像新娘。我一直安慰自己奥邮,他們只是感情好万牺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著洽腺,像睡著了一般脚粟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蘸朋,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天核无,我揣著相機(jī)與錄音,去河邊找鬼藕坯。 笑死团南,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的堕担。 我是一名探鬼主播已慢,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼霹购!你這毒婦竟也來(lái)了佑惠?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎膜楷,沒(méi)想到半個(gè)月后旭咽,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赌厅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年穷绵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片特愿。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡仲墨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出揍障,到底是詐尸還是另有隱情目养,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布毒嫡,位于F島的核電站癌蚁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏兜畸。R本人自食惡果不足惜努释,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咬摇。 院中可真熱鬧伐蒂,春花似錦、人聲如沸菲嘴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春志鹃,著一層夾襖步出監(jiān)牢的瞬間娄昆,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工佛纫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妓局,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓呈宇,卻偏偏與公主長(zhǎng)得像好爬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子甥啄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容