如何優化你的超大型React應用 【原創精讀】

clipboard.png

React爲了大型應用而生, ElectronReact-native賦予了它構建移動端跨平臺 App和桌面應用的能力, Taro則賦予了它一次編寫,生成多種平臺小程序和 React-native應用的能力,這裏特地說下 Taro,它是國產,文檔寫得比較不錯,並且它的升級速度比較快,有 issue我看也會及時解決,他們的維護人員仍是很是敬業的!


clipboard.pngcss

  • Tips:本文某些知識點若是介紹不對或者不全的地方歡迎指出,本文可能內容比較多,閱讀時間花費比較長,可是但願你能夠認真看下去,能夠的話最好手把手去實現一些code,本文全部代碼均手寫。

本文會從原生瀏覽器環境,到跨平臺開發逐漸去深刻介紹,先給一些資料

原生瀏覽器環境:

  • 原生瀏覽器環境實際上是最考驗前端工程師能力的編程環境,由於咱們前端大部分一開始面向瀏覽器編程,如今不少不少工做5-10年的前端,性能面板API都不知道用,怎麼看調用函數分析耗時都不知道,這也是最近面試的狀況,以爲有人說35歲失業的狀況,是廣泛存在,可是很大部分是你在混啊兄弟。
原生瀏覽器環境中使用React框架,比較常見的是製做單頁面SPA應用:
原生的SPA應用,分如下幾種:
  • CSR渲染(客戶端渲染)
  • SSR渲染(服務端渲染)
  • 混合渲染(預渲染,webpack的插件預渲染,Next.js的約定式路由SSR,或者使用Node.js作中間件,作部分SSR,加快首屏渲染,或者指定路由SSR.)
下面會分別仔細介紹這幾種渲染形式的精細化渲染,以及優缺點:

CSR渲染

  • 客戶端請求RestFul接口,接口吐回靜態資源文件html

    • Node.js實現代碼
const express = require('express')
const app = express()

app.use(express.static('pulic'))//這裏的public就是靜態資源的文件夾,讓客戶端拉取的,這裏的代碼是前端的代碼已經構建完畢的代碼 

app.get('/',(req,res)=>{
 //do something 
    
})

app.listen(3000,err=>{
    if(!err)=>{
        console.log('監聽端口號3000成功')
    }
})
  • 客戶端收到一個HTML文件,和若干個CSS文件,以及多個javaScript文件
  • 用戶輸入了url地址欄而後客戶端返回靜態文件,客戶端開始解析
  • 客戶端解析文件,js代碼動態生成頁面。(這也是爲何說單頁面應用的SEO不友好的緣由,初始它只是一個空的div標籤的HTML文件)
  • 判斷一個頁面是否是CSR,很大程度上能夠根據右鍵點開查看頁面元素,若是隻有一個空的div標籤,那麼大機率能夠說是單頁面,CSR,客戶端渲染的網頁。
CSR的應用,如何精細化渲染呢?

單頁面採起CSR形式,大都依賴框架,VueReact之類。一旦使用這類型技術架構,狀態數據集中管理,單向數據流,不可變數據,路由懶加載,按需加載組件,適當的緩存機制(PWA技術),細緻拆分組件,單一數據來源刷新組件,這些都是咱們能夠精細化的方向。每每純CSR的單頁面應用通常不會太複雜,因此這裏不引入PWAweb work等等,在後面複雜的跨平臺應用中我會將那些技術蜂擁而上。

  • 單一數據來源決定組件是否刷新是精細化最重要的方向。
class app extends React.PureComponent{

    ///////
}

export default connect(
 (({xx,xxx,xxxx,xxxxx}))
////

)(app)
一旦業務邏輯很是複雜的狀況下,假設咱們使用的是 dva集中狀態管理,同時鏈接這麼多的狀態樹模塊,那麼可能會形成狀態樹模塊中任意的數據刷新致使這個組件被刷新,可是其實這個組件此時是不須要刷新的。
  • 這裏能夠將須要的狀態經過根組件用props傳入,精確刷新的來源,單一可變數據來源追溯性強,也更方便debug
  • 單向數據流不可變數據,經過immutable.js這個庫實現
import Immutable from require('immutable');
    var map1: Immutable.Map<string, number>;
    map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
    var map2 = map1.set('b', 50);
    map1.get('b'); // 2
    map2.get('b'); // 50
不可變數據,數據共享,持久化存儲,經過 is比較,每次 map生成的都是惟一的 ,它們比較的是 codehash的值,性能比經過遞歸或者直接比較強不少。在 PureComponent淺比較很差用的時候
  • 通常的組件,使用PureComponent減小重複渲染便可
  • PureComponent,平時咱們建立 React 組件通常是繼承於 Component,而 PureComponent 至關因而一個更純淨的 Component,對更新先後的數據進行了一次淺比較。只有在數據真正發生改變時,纔會對組件從新進行 render。所以能夠大大提升組件的性能。
  • PureComponent部分源碼,其實就是淺比較,只不過對一些特殊值進行了判斷:
function is(x: any, y: any) {
    return (
        (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) 
    );
}
這裏特別注意,爲何使用immutable.js和pureComponent,由於React一旦根組件被刷

新,會自上而下逐漸刷新整個子孫組件,這樣性能損耗重複渲染就會多出不少,因此咱們不只要單一數據來源控制組件刷新,偶爾還須要在shouldComponentUpdate中對比nextProps和this.props 以及this.state以及nextState.前端

  • 路由懶加載+code-spliting,加快首屏渲染,也能夠減輕服務器壓力,由於不少人可能訪問你的網頁並不會看某些路由的內容
  • 使用react-loadable,支持SSR,很是推薦,官方的lazy不支持SSR,這是一個遺憾,這裏須要配合wepback4optimization配置,進行代碼分割
Tips:這裏須要下載支持動態 importbabel預設包 @babel/plugin-syntax-dynamic-import ,它支持動態倒入組件
webpack配置:

 optimization: {
        runtimeChunk: true,
        splitChunks: {
            chunks: 'all'
        }
    }
import React from 'react'
    import Loading from './loading-window'//佔位的那個組件,初始加載
    import Loadable from 'react-loadable'
    const LoadableComponent = Loadable({
        loader: () => import('./sessionWindow'),//真正須要加載的組件
        loading: Loading,
      });
      
      
    export default LoadableComponent
  • 好了,如今路由懶加載組件以及代碼分割已經作好了,並且它支持SSR。很是棒
  • 因爲純CSR的網頁通常不是很複雜,這裏再介紹一個方面,那就是,能不用redux,dva等集中狀態管理的狀態就不上狀態樹,實踐證實,頻繁更新狀態樹對用戶體驗來講是影響很是大的。這個異步的過程,更耗時。遠不如支持經過props等方式進行組件間通訊,原則上除了不少組件共享的數據才上狀態樹,不然都採用其餘方式進行通訊。

SSR,服務端渲染:

服務端渲染能夠分爲:
純服務端渲染,如jade,tempalte,ejs等模板引擎進行渲染,而後返回給前端對應的HTML文件
  • 這裏也使用Node.js+express框架
const express= require('express')
const app =express()
const jade = require('jade')
const result = ***
const url path = *** 
const html = jade.renderFile(url, { data: result, urlPath })//傳入數據給模板引擎
app.get('/',(req,res)=>{
    res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回給客戶端
}) //RestFul接口 

app.listen(3000,err=>{
    //do something
})
混合渲染,使用webpack4插件,預渲染指定路由,被指定的路由爲SSR渲染,後臺0代碼實現
const PrerenderSPAPlugin = require('prerender-spa-plugin')
new PrerenderSPAPlugin({
            routes: ['/','/home','/shop'],
            staticDir: resolve(__dirname, '../dist'),
          }),
混合渲染,使用Node.js做爲中間件,SSR指定的路由加快首屏渲染,固然CSS也能夠服務端渲染,動態Title和meta標籤,更好的SEO優化,這裏Node.js還能夠同時處理數據,減輕前端的計算負擔。
  • 我以爲掘金上的神三元那篇文章就寫得很好,後面我本身去逐步實現了一次,感受對SSR對理解更爲透徹,加上原本就天天在寫Node.js,還會一點Next,Nuxt,服務端渲染,以爲大同小異。
  • 服務端渲染本質,在服務端把代碼運行一次,將數據提早請求回來,返回運行後的html文件,客戶端接到文件後,拉取js代碼,代碼注水,而後顯示,脫水,js接管頁面。
  • 同構直出代碼,能夠大大下降首屏渲染時間,通過實踐,根據不一樣的內容和配置能夠縮短40%-65%時間,可是服務端渲染會給服務器帶來壓力,因此折中根據狀況使用。
  • 如下是一個最簡單的服務端渲染,服務端直接吐拼接後的html結構字符串:
var express = require('express')
var app = express()

app.get('/', (req, res) => {
 res.send(
 `
   <html>
     <head>
       <title>hello</title>
     </head>
     <body>
       <h1>hello world </h1>
     </body>
   </html>
 `
 )
})

app.listen(3000, () => {
 if(!err)=>{
console.log('3000監聽')Ï
}
})
只要客戶端訪問 localhost:3000就能夠拿到數據頁面訪問
服務端渲染核心,保證代碼在服務端運行一次,將reduxstore狀態樹中的數據一塊兒返回給客戶端,客戶端脫水,渲染。 保證它們的狀態數據和路由一致,就能夠說是成功了。必需要客戶端和服務端代碼和數據一致性,不然SSR就算失敗。
//server.js

// server/index.js
import express from 'express';
import { render } from '../utils';
import { serverStore } from '../containers/redux-file/store';
const app = express();
app.use(express.static('public'));
app.get('*', function(req, res) {
  if (req.path === '/favicon.ico') {
    res.send();
    return;
  }
  const store = serverStore();
  res.send(render(req, store));
});
const server = app.listen(3000, () => {
  var host = server.address().address;
  var port = server.address().port;
  console.log(host, port);
  console.log('啓動鏈接了');
});


//render函數
import Routes from '../Router';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Link, Route } from 'react-router-dom';
import React from 'react';
import { Provider } from 'react-redux';
import { renderRoutes } from 'react-router-config';
import routers from '../Router';
import { matchRoutes } from 'react-router-config';
export const render = (req, store) => {
  const matchedRoutes = matchRoutes(routers, req.path);
  matchedRoutes.forEach(item => {
    //若是這個路由對應的組件有loadData方法
    if (item.route.loadData) {
      item.route.loadData(store);
    }
  });
  console.log(store.getState(),Date.now())
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>{renderRoutes(routers)}</StaticRouter>
    </Provider>
  );
  return `
      <html>
        <head>
          <title>ssr123</title>
        </head>
        <body>
          <div id="root">${content}</div>
          <script>window.context={state:${JSON.stringify(store.getState())}}</script>
          <script src="/index.js"></script>
        </body>
      </html>
    `;
};
  • 數據注水,脫水,保持客戶端和服務端store的一致性。
上面返回的 script標籤,裏面已經注水,將在服務端獲取到的數據給到了全局window下的context屬性,在初始化客戶端 store時候咱們給它脫水。初始化渲染使用服務端獲取的數據~
import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';
import reducers from './reducers';

export const getClientStore = () => {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducers, defaultState, applyMiddleware(thunk));
};

export const serverStore = () => {
  return createStore(reducers, applyMiddleware(thunk));
};
  • 這裏注意,在組件的componentDidMount生命週期中發送ajax等獲取數據時候,先判斷下狀態樹中有沒有數據,若是有數據,那麼就不要重複發送請求,致使資源浪費。
  • 多層級路由SSR
//路由配置文件,改爲這種方式
import Home from './containers/Home';
import Login from './containers/Login';
import App from './containers/app';
export default [
  {
    component: App,
    routes: [
      {
        path: '/',
        component: Home,
        exact: true,
        loadData: Home.loadData
      },
      {
        path: '/login',
        component: Login,
        exact: true
      }
    ]
  }
];
  • 入口文件路由部分改爲:
server.js

 const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path}>{renderRoutes(routers)}</StaticRouter>
    </Provider>
  );

client.js 

 <Provider store={store}>
      <BrowserRouter>{renderRoutes(routers)}</BrowserRouter>
    </Provider>
  • 後續可能有利用loader進行CSS的服務端渲染以及helmet的動態meta, title標籤進行SEO優化等,今天時間緊促,就不繼續寫SSR了。

構建Electron極度複雜,超大數據的應用。

須要用到技術,sqlite,PWA,web work,原生Node.js,react-window,react-lazyload,C++插件等
  • 第一個提到的是sqlite,嵌入式關係型數據庫,輕量型無入侵性,標準的sql語句,這裏不作過多介紹。
  • PWA,漸進性式web應用,這裏使用webpack4的插件,進行快速使用,對於一些數據內容不須要存儲數據庫的,可是卻想要一次拉取,屢次複用,那麼可使用這個配置

serverce work也有它的一套生命週期

clipboard.png

  • 一般咱們若是要使用 Service Worker 基本就是如下幾個步驟:
  • 首先咱們須要在頁面的 JavaScript 主線程中使用 serviceWorkerContainer.register() 來註冊 Service Worker ,在註冊的過程當中,瀏覽器會在後臺啓動嘗試 Service Worker 的安裝步驟。
  • 若是註冊成功,Service Worker 在 ServiceWorkerGlobalScope 環境中運行; 這是一個特殊的 worker context,與主腳本的運行線程相獨立,同時也沒有訪問 DOM 的能力。
  • 後臺開始安裝步驟, 一般在安裝的過程當中須要緩存一些靜態資源。若是全部的資源成功緩存則安裝成功,若是有任何靜態資源緩存失敗則安裝失敗,在這裏失敗的沒關係,會自動繼續安裝直到安裝成功,若是安裝不成功沒法進行下一步 — 激活 Service Worker。
  • 開始激活 Service Worker,必需要在 Service Worker 安裝成功以後,才能開始激活步驟,當 Service Worker 安裝完成後,會接收到一個激活事件(activate event)。激活事件的處理函數中,主要操做是清理舊版本的 Service Worker 腳本中使用資源。
  • 激活成功後 Service Worker 能夠控制頁面了,可是隻針對在成功註冊了 Service Worker 後打開的頁面。也就是說,頁面打開時有沒有 Service Worker,決定了接下來頁面的生命週期內受不受 Service Worker 控制。因此,只有當頁面刷新後,以前不受 Service Worker 控制的頁面纔有可能被控制起來。
直接上代碼,存儲全部 js文件和圖片 //實際的存儲根據自身須要,並非越多越好。
const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
            clientsClaim: true,
            skipWaiting: true,
            importWorkboxFrom: 'local',
            include: [/\.js$/, /\.css$/, /\.html$/, /\.jpg/, /\.jpeg/, /\.svg/, /\.webp/, /\.png/],
        }),
  • PWA並不只僅這些功能,它的功能很是強大,有興趣的能夠去lavas看看,PWA技術對於常常訪問的老客戶來講,首屏渲染提高很是大,特別在移動端,能夠添加到桌面保存。666啊~,在pc端更多的是緩存處理文件~
  • 使用react-lazyload,懶加載你的視窗初始看不見的組件或者圖片。
/開箱即用的懶加載圖片
import LazyLoad from 'react-lazyload'
 <LazyLoad height={42} offset={100} once> //這裏配置表示佔位符的樣式~。
          <img
            src={this.state.src}
            onError={this.handleError.bind(this)}
            className={className || 'avatar'}
          />
</LazyLoad>

記得在移動端的滑動屏幕或者PC端的調用forceCheck,動態計算元素距離視窗的位置而後決定是否顯示真的圖片~

import { forceCheck } from 'react-lazyload';
forceCheck()
  • 懶加載組件
import { lazyload } from 'react-lazyload';
//跟上面同理,不過是一個裝飾器,高階函數而已。同樣須要forcecheck()
@lazyload({
  height: 200,
  once: true,
  offset: 100
})
class MyComponent extends React.Component {
  render() {
    return <div>this component is lazyloaded by default!</div>;
  }
}

大數據React渲染,擁有讓應用擁有60FPS -很是核心的一點優化

  • List長列表

clipboard.png
]java

  • react-virtualized-auto-sizer和windowScroll配合一塊兒使用,達到頁面複雜效果+大數據渲染保持60FPS。上面的官網裏有介紹這些組件~

高計算量的工做交給web wrok線程

var myWorker = new Worker('worker.js'); 
first.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

second.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}
  • 這段代碼中變量first和second表明2個<input>元素;它們當中任意一個的值發生改變時,myWorker.postMessage([first.value,second.value])會將這2個值組成數組發送給worker。你能夠在消息中發送許多你想發送的東西。
  • 在worker中接收到消息後,咱們能夠寫這樣一個事件處理函數代碼做爲響應(worker.js):
onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}
  • onmessage處理函數容許咱們在任什麼時候刻,一旦接收到消息就能夠執行一些代碼,代碼中消息自己做爲事件的data屬性進行使用。這裏咱們簡單的對這2個數字做乘法處理並再次使用postMessage()方法,將結果回傳給主線程。
  • 回到主線程,咱們再次使用onmessage以響應worker回傳的消息:
myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}
  • 在這裏咱們獲取消息事件的data,而且將它設置爲result的textContent,因此用戶能夠直接看到運算的結果。
  • 注意: 在主線程中使用時,onmessage和postMessage() 必須掛在worker對象上,而在worker中使用時不用這樣作。緣由是,在worker內部,worker是有效的全局做用域。
  • 注意: 當一個消息在主線程和worker之間傳遞時,它被複制或者轉移了,而不是共享。
開啓 web work線程,其實也會損耗必定的主線程的性能,可是大量計算的工做交給它也何嘗不可,其實 Node.jsjavaScript都不適合作大量計算工做,這點有目共睹,尤爲是 js引擎和 GUI渲染線程互斥的狀況存在。

充分合理利用ReactFeber架構diff算法優化項目

  • requestAnimationFrame調用高優先級任務,中斷調度階段的遍歷,因爲React的新版本調度階段是擁有三根指針的可中斷的鏈表遍歷,因此這樣既不影響下面的遍歷,也不影響用戶交互等行爲。

clipboard.png

  • 使用requestAnimationFrame,當頁面處於未激活的狀態下,該頁面的屏幕刷新任務會被系統暫停,因爲requestAnimationFrame保持和屏幕刷新同步執行,因此也會被暫停。當頁面被激活時,動畫從上次停留的地方繼續執行,節約 CPU 開銷。
  • 一個刷新間隔內函數執行屢次時沒有意義的,由於顯示器每 16.7ms 刷新一次,屢次繪製並不會在屏幕上體現出來
  • 在高頻事件(resize,scroll等)中,使用requestAnimationFrame能夠防止在一個刷新間隔內發生屢次函數執行,這樣保證了流暢性,也節省了函數執行的開銷

某些狀況下能夠直接使用requestAnimationFrame替代 Throttle 函數,都是限制回調函數執行的頻率react

使用 requestAnimationFrame也能夠更好的讓瀏覽器保持60幀的動畫
  • requestIdleCallback,這個API目前兼容性不太好,可是在Electron開發中,可使用,二者仍是有區別的,並且這兩個api用好了能夠解決不少複雜狀況下的問題~。固然你也能夠用上面的api封裝這個api,也並非很複雜。

clipboard.png

  • 當關注用戶體驗,不但願由於一些不重要的任務(如統計上報)致使用戶感受到卡頓的話,就應該考慮使用requestIdleCallback。由於requestIdleCallback回調的執行的前提條件是當前瀏覽器處於空閒狀態。
  • 圖中一幀包含了用戶的交互、js的執行、以及requestAnimationFrame的調用,佈局計算以及頁面的重繪等工做。

假如某一幀裏面要執行的任務很少,在不到16ms(1000/60)的時間內就完成了上述任務的話,那麼這一幀就會有必定的空閒時間,這段時間就剛好能夠用來執行requestIdleCallback的回調,以下圖所示:webpack

clipboard.png

使用preloadprefetch,dns-prefetch等指定提早請求指定文件,或者根據狀況,瀏覽器自行決定是否提早dns預解析或者按需請求某些資源。

  • 這裏也能夠webpack4插件實現,目前京東在使用這個方案~
const PreloadWebpackPlugin = require('preload-webpack-plugin')
 new PreloadWebpackPlugin({
            rel: 'preload',
            as(entry) {
              if (/\.css$/.test(entry)) return 'style';
              if (/\.woff$/.test(entry)) return 'font';
              if (/\.png$/.test(entry)) return 'image';
              return 'script';
            },
            include:'allChunks'
            //include: ['app']
          }),

對指定js文件延遲加載~

  • 普通的腳本

clipboard.png

  • script標籤,加上async標籤,遇到此標籤,先去請求,可是不阻塞解析html等文件~,請求回來就立馬加載

clipboard.png

  • script標籤,加上defer標籤,延遲加載,可是必須在全部腳本加載完畢後纔會加載它,可是這個標籤有bug,不肯定可否準時加載。通常只給一個

clipboard.png

寫這篇時間太耗時間,並且論壇的在線編輯器到了內容不少的時候,很是卡, React-native的以及一些細節,後面再補充

下面給出一些源碼和資料地址:

相關文章
相關標籤/搜索