原文一個月前發佈在簡聊(https://jianliao.com/)博客上, 這邊作一下備份
https://jianliao.com/blog/jian-liao-shou-ping-xing-neng-you-hua-fang-an-xie-ji-lu/php
首先整個改進方案的基礎是 Redux 提出的 Single Store 架構
按照 Redux 的理念對應用進行抽象之後, 架構迴歸到 MVC 很是原始的理念,
也就是: 一個 Model, 一個 View, 以及剩下的 Controller 代碼react
我認爲存在問題的方案是對象化的封裝, 特別是每一個 View 存在獨立的相似 MVC 的對象,
具體來是簡聊早期使用的 Backbone 架構, 數據分散在各個 Collection 當中, 難以管理git
到了 Redux 的架構中, 只有一個 Store, 對數據進行統一管理就方便多了
而 immutable-js 的加入, 更讓整個架構的數據流變得很是清晰
Redux 方案裏, 數據層是 Store, 界面是 React Component 組成的 View
而 Controller 的只能由 Actions 和 Reducer 來承擔, 這裏只是作個類比github
爲了從簡單的數據結構和函數構建複雜的應用, 每一個部分都要進行抽象和複合
首先 View 藉助 React Components 從小到大很靈活地進行組合
而 Store 經過 Map 和 Reducer 函數也能組合(其實這一點咱們沒有貫徹)
此外, 應用和服務端存在數據同步的需求, 也須要考慮抽象(後面討論). 總體架構大體就是這些api
Redux 最知名的是它的 Time Travel Debugging 功能, 也就是記錄 Actions 和 Store 進行回溯. 實際上這也是檢驗"數據界面分離是否充分"這樣一個架構是否完善的一個考驗
當一個單頁面應用能自由地回滾數據狀態而不引起異常, 才能夠更有信心地說應用的行爲和數據流很是清晰, 很好預測
同時, 界面和數據沒有複雜的雙向的操做, 特別是渲染界面時致使數據更新
React 組件任意地渲染更新界面, 並且數據不受影響, 應用纔不會走向混亂瀏覽器
在實際的應用編寫當中, 切換頁面的加載數據過程, 存在具體的問題
早先咱們的架構當中切換路由, 是先切換界面, 而後界面初始化時加載數據
可是這個作法就違背上面定下來的方案了, 就是渲染過程存在數據操做
另外一個實際的影響是, 請求數據的邏輯是在組件掛載時才調用的, 並很差優化
設想一下當咱們想加載數據, 卻要先去渲染界面, 這樣的架構是否清晰?
固然, 對我來講最頭疼的仍是前面的, 阻礙我從更高層次對架構進行優化的問題緩存
好比說簡聊切換話題, 點擊話題, 地址改變, 就須要加載數據和渲染界面
按照默認的 react-router 的行爲, 地址改變將直接致使界面重繪
也就是新的話題的界面立刻就被渲染出來,然而次數話題的數據尚未請求到
話題的組件被迫渲染沒有數據的話題, 等到數據加載完成, 再從新渲染一遍
這個界面不少行爲超出控制, 難以優化, 特別是路由不受控制
正確的流程, 應當是先加載數據, 再渲染界面, 以及加載過程作一些提示性能優化
基於上邊的架構設計和具體問題, 簡聊採用了本身實現的路由組件
這個路由組件的行爲經過 Single Store 中的數據控制, 以及相應的 loading 狀態
如今版本的簡聊, 點擊話題, 會先標記 loading 狀態, 同時在後臺發起數據請求,
請求完成, 重置 loading 狀態的同時, 請求到的數據在界面上被現實出來
以及, 如今也能夠對數據請求操做進行合併, 這點將方便將來的優化
包括切換團隊, 如今也能作到優化, 達成先加載數據而後渲染界面的效果網絡
爲了處理好這個過程, 還須要知道的是, 對應的路由須要哪些數據
好比說, 在話題 A 裏邊, 咱們須要 topicA, memberA, team1, contact1
而在話題 B 裏邊, 咱們須要 topicB, memberB, team1, contact1 的數據
從 A 切換到 B, 就須要分析本地混存已有哪些數據, 缺乏那些數據
而後才能準確地抓取缺乏的數據, 在界面須要的數據抓取完成時開始渲染
Facebook Relay 就是以此爲目的的一套方案, 只是並不適合咱們
所以項目中本身實現了簡單的數據依賴分析代碼, 達成了這個目的數據結構
先說簡聊有一個針對斷網和從新鏈接作的特殊處理, 會強制更新一遍本地的數據
具體說就是清楚掉內存緩存的舊的消息和話題, 或者或標記全部的緩存失效
而後, 會按照前文描述的方案分析當前須要的數據, 進行一次數據抓取
其實這個過程的關鍵是, 須要根據路由分析出當前界面須要哪些數據
由於簡聊主要的狀態是存儲在 Store 當中, 至少在架構上不存在障礙
通過這樣的操做, 網絡從新鏈接之後, 簡聊能夠恢復到一個同步更新的狀態
而首屏加載是類似的問題, 用戶一段時間沒有登陸, 這時須要進行一次同步
區別只是在於, 首屏加載本地並無對應緩存, 須要拿到數據才能開始渲染
抓取更新數據的問題, 前面講的網絡重連的代碼徹底能夠重用
而後是緩存, 跳得遠一點, 前面說了, 簡聊是 Single Store, 主要數據存放在一塊兒的
因而, 就頗有可能, 把 Single Store 存儲下來, 做爲下次頁面打開的緩存使用
也就是說, 至關於關閉瀏覽器時把一切緩存下來, 下次打開時一切復原到關閉時的情景
而實際上用戶經過 jianliao.com 訪問應用, 極可能就命中這個緩存了
基於這樣的出發點, 簡聊在在關閉應用時將 Store 轉化爲 JSON 字符串存進 localStorage
應用再次打開時, 若是條件合適命中緩存, 就直接將緩存渲染出來
而且在後臺分析依賴開始抓數據, 最終在得到新數據以後再次更新界面
對於用戶來講, 瀏覽器輸入 jianliao.com 就當即開始渲染, 省去很多的等待時間
若是不是太長時間沒有登陸簡聊, 新數據更新的界面也不會太明顯
最直接的效果就是不少狀況下簡聊能夠更快地打開了, 也就更方便
在這個花招使用的先後, 簡聊加載的順序發生了一些變化
以前: 加載資源, 運行代碼, 請求數據, 渲染界面 以後: 加載資源, 運行代碼, 渲染界面, 後臺請求數據, 局部更新界面
去掉了基數和波動較大的網絡請求時間, 剩下的就是 JavaScript 執行和渲染的時間了
想要讓簡聊首屏更快地出如今用戶面前, 就要開始優化啓動和渲染速度
經過 Chrome 的 Timeline 調試工具, 我逐步收集到的大概有這些點
主要是 DOM reflow 的開銷, 不穩定然而一般致使較長的時間消耗
(下面的列表是我主要基於記憶列出的, 並非完整的):
getBoundingClientRect
調用, 關係到一些菜單定位(能夠優化)
focus
操做, 有時會觸發 DOM 的 reflow(有的不能優化, 但能夠延時處理)
scrollTop
讀取和操做比較明顯(然而部分調用不能優化, 不然影響到用戶體驗)
jQuery 初始化過程會讀寫 DOM, 可能觸發 reflow (目前還不能去掉)
React 初始化過程會作一些 DOM 的探測(沒法優化)
rangy 初始化過程有 DOM 讀寫(沒法優化)
favico.js 初始化有 DOM 的讀寫(沒法優化)
問題的關鍵固然是 DOM 操做觸發了一些 reflow 的問題
其次 JavaScript 初始化各類 timer 和複雜計算也會消耗時間
但實際上純 JavaScript 自己性能, 只要代碼不寫出問題, 也不會慢了
只是對 DOM, 仍是要關心挺多, 這方面直接推薦網上的資料了:
http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/
https://gist.github.com/paulirish/5d52fb081b3570c81e3a
考慮到這是剩下的主要是 CPU 密集的計算, 實際上受到 CPU 性能影響較大CPU 性能越好, 渲染也就越快. 至少如今已經部分地去掉了網絡慢的影響另外, 咱們目前對渲染作的優化只是初步, 相信後續的深刻優化之後初次渲染的性能還會有一些提高