[TOC]
# 時間分片
## 使用定時器
在 JS 的`Event Loop`中,當JS引擎所管理的執行棧中的事件以及所有微任務事件全部執行完后,才會觸發渲染線程對頁面進行渲染。
頁面的卡頓是由于同時渲染大量DOM所引起的,所以我們考慮將渲染過程分批進行。
<br>
## requestAnimationFrame
### setTimeout 和閃屏現象
* `setTimeout`的執行時間并不是確定的。在JS中,`setTimeout`任務被放進事件隊列中,只有主線程執行完才會去檢查事件隊列中的任務是否需要執行,因此`setTimeout`的實際執行時間可能會比其設定的時間晚一些。
* 刷新頻率受屏幕分辨率和屏幕尺寸的影響,因此不同設備的刷新頻率可能會不同,而`setTimeout`只能設置一個固定時間間隔,這個時間不一定和屏幕的刷新時間相同。
以上兩種情況都會導致setTimeout的執行步調和屏幕的刷新步調不一致。
在`setTimeout`中對dom進行操作,必須要等到屏幕下次繪制時才能更新到屏幕上,如果兩者步調不一致,就可能導致中間某一幀的操作被跨越過去,而直接更新下一幀的元素,從而導致丟幀現象。
<br>
### 使用 requestAnimationFrame
與`setTimeout`相比,`requestAnimationFrame`最大的優勢是由系統來決定回調函數的執行時機。
如果屏幕刷新率是60Hz,那么回調函數就每16.7ms被執行一次,如果刷新率是75Hz,那么這個時間間隔就變成了1000/75=13.3ms,換句話說就是,`requestAnimationFrame`的步伐跟著系統的刷新步伐走。它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引起丟幀現象。
<br>
## DocumentFragment
`DocumentFragments`是DOM節點,但并不是DOM樹的一部分,可以認為是存在內存中的,所以將子元素插入到文檔片段時不會引起頁面回流。
<br>
## 最終代碼
~~~
<ul id="container"></ul>
~~~
~~~
//需要插入的容器
let ul = document.getElementById('container');
// 插入十萬條數據
let total = 100000;
// 一次插入 20 條
let once = 20;
//總頁數
let page = total/once
//每條記錄的索引
let index = 0;
//循環加載數據
function loop(curTotal,curIndex){
if(curTotal <= 0){
return false;
}
//每頁多少條
let pageCount = Math.min(curTotal , once);
window.requestAnimationFrame(function(){
let fragment = document.createDocumentFragment();
for(let i = 0; i < pageCount; i++){
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount,curIndex + pageCount)
})
}
loop(total,index);
~~~
<br>
<br>
# worker
## 什么是worker
~~~
運行者 Worker 接口是Web Workers API的一部分,代表一個后臺任務,
它容易被創建并向創建者發回消息。創建一個運行者只要簡單的調用Worker()構造函數,指定一個腳本,在工作線程中執行。(引自MDN)
~~~
看概念可能有點枯燥,通俗點講就是:因為js是單線程運行的,在遇到一些需要處理大量數據的js時,可能會阻塞頁面的加載,造成頁面的假死。這時我們可以使用worker來開辟一個獨立于主線程的子線程來進行哪些大量運算。這樣就不會造成頁面卡死。也說明 worker可以用來解決大量運算是造成頁面卡死的問題。
<br>
<br>
## 語法
### 創建 Web Workers
~~~
const worker=new Worker(aURL, options)
~~~
它有兩個參數:
* aURL(必須)是一個DOMString 表示worker 將執行的腳本的URL。它必須遵守同源策略。
* options (可選)它的一個作用就是指定 Worker 的名稱,用來區分多個 Worker 線程
<br>
### 收發消息
Web Workers 用來執行異步腳本,只要掌握了它與主線程通信的方式,就可以在指定時機運行異步腳本,并在運行完時將結果傳遞給主線程。
<br>
### 主線程接收發 Web Workers 消息
~~~text
const worker = new Worker("../src/worker.js");
worker.onmessage = e => {};
worker.postMessage("Marco!");
~~~
每個`worker`實例通過`onmessage`接收消息,通過`postMessage`發送消息。
<br>
### Web Workers 收發主線程消息
~~~text
self.onmessage = e => {};
self.postMessage("Marco!");
~~~
和主線程代碼類似,在 Web Workers 代碼中,也是`onmessage`接收消息,這個消息來自主線程或者其它 Workers。也可以通過`postMessage`發送消息。
<br>
### 銷毀 Web Workers
~~~text
worker.terminate();
~~~
文章內容就這么多,是不是有寫太簡單了呢!筆者結合自己的使用經驗,再補充一些知識。
<br>
<br>
## 使用worker的注意點
<br>
### 1.同源限制
分配給 Worker 線程運行的腳本文件,必須與主線程的腳本文件同源。
<br>
### 2.DOM 限制
Worker 線程所在的全局對象,與主線程不一樣,無法讀取主線程所在網頁的 DOM 對象,也無法使用document、window、parent這些對象。但是,Worker 線程可以navigator對象和location對象。
<br>
### 3.通信聯系
Worker 線程和主線程不在同一個上下文環境,它們不能直接通信,必須通過消息完成。
<br>
### 4.腳本限制
Worker 線程不能執行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 對象發出 AJAX 請求。
<br>
### 5.文件限制
Worker 線程無法讀取本地文件,即不能打開本機的文件系統(file://),它所加載的腳本,必須來自網絡。
<br>
## 優化
### 對象轉移(Transferable Objects)
對象轉移就是將對象引用零成本轉交給 Web Workers 的上下文,而不需要進行結構拷貝。
這里要解釋的是,**主線程與 Web Workers 之間的通信,并不是對象引用的傳遞,而是序列化/反序列化的過程**,當對象非常龐大時,序列化和反序列化都會消耗大量計算資源,降低運行速度。

上面的圖充分證明了,大對象傳遞,使用對象轉移各項指標都優于結構拷貝。
對象轉移使用方式很簡單,給`postMessage`增加一個參數,把對象引用傳過去即可:
~~~text
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
~~~
瀏覽器兼容性也不錯:Currently Chrome 17+, Firefox, Opera, Safari, IE10+。更具體內容,可以看[Transferable Objects: Lightning Fast!](https://link.zhihu.com/?target=https%3A//developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast)。
> 需要注意的是,對象引用轉移后,原先上下文就無法訪問此對象了,需要在 Web Workers 再次將對象還原到主線程上下文后,主線程才能正常訪問被轉交的對象。
### 如何不用 JS 文件創建 Web Workers
Web Workers 優勢這么大,但用起來需要在同域下創建一個 JS 文件實在不方便,尤其在前后端分離做的比較徹底的團隊,前端團隊能控制的僅僅是一個 JS 文件。那么下面給出幾個不用 JS 文件,就創建 Web Workers 的方法:
### webpack 插件 - worker-loader
[worker-loader](https://link.zhihu.com/?target=https%3A//github.com/webpack-contrib/worker-loader)是一個 webpack 插件,可以將一個普通 JS 文件的全部依賴提取后打包并替換調用處,以 Blob 形式內聯在源碼中。
~~~text
import Worker from "worker-loader!./file.worker.js";
const worker = new Worker();
~~~
上述代碼的魔術在于,轉化成下面的方式執行:
~~~text
const blob = new Blob([codeFromFileWorker], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
~~~
### Blob URL
第二種方式由第一種方式自然帶出:如果不想用 webpack 插件,那自己通過 Blob 的方式創建也可以:
~~~text
const code = `
importScripts('https://xxx.com/xxx.js');
self.onmessage = e => {};
`;
const blob = new Blob([code], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
~~~
看上去代碼更輕量一些,不過問題是當遇到復雜依賴時,如果不能把依賴都轉化為腳本通過`importScripts`方式引用,就無法訪問到主線程環境中的包。如果真的遇到了這個問題,可以用第一種 webpack 插件的方式解決,這個插件會自動把文件所有依賴都打包進源碼。
### 管理 postMessage 隊列
為什么 postMessage 會形成隊列,為什么要管理它?
首先在 Web Workers 架構設計上就必須做成隊列,因為調用`postMessage`時,對應的 Web Workers 不一定完成了初始化,所以瀏覽器底層必須管理一個隊列,在 Web Workers 初始化完畢時,依次消費,這樣才能確保任何時候發出的`postMessage`都能被 Web Workers 接收到。
其次,為什么要手動維護這個隊列,原因可能取決于如下幾點:
* 業務原因,前面的`postMessage`還沒來得及消費,就不要發送新的消息,或者丟棄新的消息,這時候需要通過雙向通信拿到 Web Workers 的執行結果回執,手動控制隊列。
* 性能原因,一般 Web Workers 都會被用來執行耗時的同步運算,如果運算時間比較長,那短期塞入多個消息隊列是沒有意義的。

如上圖所示,對于每次用戶輸入都要進行的 SQL Parser 很耗時,及時放在 Web Workers 也可能導致將 Workers 撐爆到無響應,這是不僅要使用多 Workers 緩沖池,還要對待執行隊列進行過濾,因為用戶永遠只關心最后一次輸入的 Parser 結果。
由于 Web Workers 運算被卡住時,除了銷毀 Worker 沒有別的辦法,而銷毀 Worker 的成本比較高,不能對每一個用戶輸入都銷毀并新建 Web Workers,所以利用 Workers 緩沖池,當緩沖池滿了,新的消費隊列又進來的時候,可以銷毀全部 Workers 緩沖池,換一批新緩沖池重新消費用戶輸入。
<br>
<br>
# 虛擬列表
https://juejin.cn/post/6844903982742110216#heading-5
<br>
<br>
# 參考資料
* [「中高級前端」高性能渲染十萬條數據(時間分片)](https://juejin.im/post/5d76f469f265da039a28aff7)
* [ 聊聊前端開發中的長列表](https://zhuanlan.zhihu.com/p/26022258)
* [再談前端虛擬列表的實現](https://zhuanlan.zhihu.com/p/34585166)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼