1. chrome devtool 是診斷頁面滾動性能的有效工具javascript
2. 提高滾動時性能,就是要達到fps高且穩。css
3. 具體能夠從如下方面着手html
滾動行爲無時無刻不出如今咱們瀏覽網頁的行爲中,在許多場景中,咱們有有意識地、主動地去使用滾動操做,好比:前端
以上場景伴隨着滾動事件的監聽操做,一不留神可能就讓頁面的滾動再也不「如絲般順滑」。java
不擇手段打造一個卡頓的scroll場景:git
做爲一名優秀的前端工程師(將來的),怎麼能允許出現這種狀況!不就性能優化嗎,撩起袖子就是幹!github
在一個流暢的頁面變化效果中(動畫或滾動),渲染幀,指的是瀏覽器從js執行到paint的一次繪製過程,幀與幀之間快速地切換,因爲人眼的殘像錯覺,就造成了動畫的效果。那麼這個「快速」,要達到多少才合適呢?web
咱們都知道,下層建築決定了上層建築。受限於目前大多數屏幕的刷新頻率——60次/s,瀏覽器的渲染更新的頁面的標準幀率也爲60次/s--60FPS(frames/per second)。算法
來個比喻。快遞天天整理包裹,並一天一送。若是某天包裹太多,整理花費了太多時間,來不及當日(幀)送到收件人處,那就延期了(丟幀)。chrome
那麼在這16.7ms以內,瀏覽器都幹了什麼呢?
瀏覽器心裏OS:不要老抱怨我延期(丟幀),我也很忙的好伐?
幀維度解釋幀渲染過程
瀏覽器渲染頁面的Renderer進程裏,涉及到了兩個線程,兩者之間經過名爲Commit的消息保持同步:
標準渲染幀:
在一個標準幀渲染時間16.7ms以內,瀏覽器須要完成Main線程的操做,並commit給Compositor進程
丟幀:
主線程裏操做太多,耗時長,commit的時間被推遲,瀏覽器來不及將頁面draw到屏幕,這就丟失了一幀
那麼Main線程裏都有些什麼操做會致使延時呢?
進一步解釋瀏覽器主要執行步驟
理論上,每次標準的渲染,瀏覽器Main線程須要執行JavaScript => Style => Layout => Paint => Composite五個步驟,可是實際上,要分場景。
指路官網
再進一步解釋瀏覽器渲染流程
流程:
1.Compositor線程接收一個vsync信號,表示這一幀開始
2.Compositor線程接收用戶的交互輸入(好比touchmove、scroll、click等)。而後commit給Main線程,這裏有兩點規則須要注意:
3.Main線程執行從JavaScript到Composite的過程,也有兩點須要注意:
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,單獨搭了個簡易廁所。
之因此說這是一個簡易廁所,由於它有一些限制
主線程和 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); };
代碼說明:
現場還原:
案例總結:
從兩次alert以後的段落滾動狀況(第二次根本動不了),足以看出大量繁雜的js計算對頁面的影響。恰當地使用web worker,能有效緩解頁面scroll阻塞的狀況。
並且它的支持率也良好~
在應用方面,Angular已經作了一些嘗試。
解密Angular WebWorker Renderer (一):想辦法打破web worker自己不能操做dom元素等限制,利用web worker執行渲染操做
函數節流與函數去抖
針對scroll事件中的回調,思路之一是對事件進行「稀釋」,減小事件回調的執行次數。
這就涉及到兩個概念:函數節流和函數去抖
有人這樣比喻:
就像一窩蜂的人去排隊看演出,隊伍很亂,看門的老大爺每隔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; };
對比以上代碼,咱們能夠發現,兩種方法應用的場景時有差異的
相對於屢次觸發只執行一次的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性能的操做應用於不一樣的場景:
案例:在這個世界上,有一種經典的導航欄形式,那就是,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的值),就會當即執行隊列裏的操做。
因此咱們知道了,就是這個特殊操做會影響瀏覽器正常的執行和渲染,假設咱們頻繁執行這樣的特殊操做,就會打斷瀏覽器原來的節奏,增大開銷。
而這個特殊操做,具體指的就是:
See more:What forces layout / reflow
解決辦法呢,有倆:
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.一些屬性會讓元素們建立出不一樣的渲染層
2.達成一些條件,渲染層會提高爲合成層
提高爲合成層幹什麼呢?普通的渲染層普通地渲染,用普通的順序普通地合成很差嗎?非要搞啥特殊待遇!
瀏覽器就說了:我這也是爲了你們共同進步(提高速度)!看那些搞特殊待遇的,都是一些拖咱們隊伍後腿的(性能開銷大),分開處理,才能保證整個隊伍穩定快速的進步!
特殊待遇:合成層的位圖,會交由 GPU 合成,比 CPU 處理要快。當須要 repaint 時,只須要 repaint 自己,不會影響到其餘的層。
對佈局屬性進行動畫,瀏覽器須要爲每一幀進行重繪並上傳到 GPU 中
對合成屬性進行動畫,瀏覽器會爲元素建立一個獨立的複合層,當元素內容沒有發生改變,該層就不會被重繪,瀏覽器會經過從新複合來建立動畫幀
因此,從合成層出發,爲了優化scroll性能,咱們能夠作這些:
提高合成層的有效方式,應用這個屬性,其實是提早通知瀏覽器,爲接下來的動畫效果操做作準備。值得注意的是
示例:
will-change: scroll-position // 表示開發者但願在不久後改變滾動條的位置或者使之產生動畫。
而後,國際慣例【並不,附上兼容性
除此以外
總結
從合成層的角度做爲性能提高的下手方向,是值得確定的,可是具體採用什麼樣的方案,仍是要先切實地分析頁面的實際性能表現,根據不一樣的場景,綜合考慮方案的得失,再總結出正確的優化途徑。
what's more
使用css屬性代替js「模擬操做」
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操做