摘要:往往純的單頁面應用一般不會太復雜,所以這里不引入和等等,在后面復雜的跨平臺應用中我會將那些技術一擁而上。構建極度復雜,超大數據的應用。
React為了大型應用而生,Electron和React-native賦予了它構建移動端跨平臺App和桌面應用的能力,Taro則賦予了它一次編寫,生成多種平臺小程序和React-native應用的能力,這里特意說下 Taro,它是國產,文檔寫得比較不錯,而且它的升級速度比較快,有issue我看也會及時解決,他們的維護人員還是非常敬業的!
,
Tips:本文某些知識點如果介紹不對或者不全的地方歡迎指出,本文可能內容比較多,閱讀時間花費比較長,但是希望你可以認真看下去,可以的話最好手把手去實現一些code,本文所有代碼均手寫。
本文會從原生瀏覽器環境,到跨平臺開發逐漸去深入介紹,先給一些資料手寫React優化腳手架帶項目
react-ssr的源碼
手寫Node.js原生靜態資源服務器
跨平臺Electron的demo
原生瀏覽器環境:原生瀏覽器環境其實是最考驗前端工程師能力的編程環境,因為我們前端大部分一開始面向瀏覽器編程,現在很多很多工作5-10年的前端,性能面板API都不知道用,怎么看調用函數分析耗時都不知道,這也是最近面試的情況,覺得有人說35歲失業的情況,是普遍存在,但是很大部分是你在混啊兄弟。
純CSR渲染(客戶端渲染)
純SSR渲染(服務端渲染)
混合渲染(預渲染,webpack的插件預渲染,Next.js的約定式路由SSR,或者使用Node.js做中間件,做部分SSR,加快首屏渲染,或者指定路由SSR.)
客戶端請求RestFul接口,接口吐回靜態資源文件
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,客戶端渲染的網頁。
單一數據來源決定組件是否刷新是精細化最重要的方向。
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; 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) ); }
新,會自上而下逐漸刷新整個子孫組件,這樣性能損耗重復渲染就會多出很多,所以我們不僅要單一數據來源控制組件刷新,偶爾還需要在shouldComponentUpdate中對比nextProps和this.props 以及this.state以及nextState.
路由懶加載+code-spliting,加快首屏渲染,也可以減輕服務器壓力,因為很多人可能訪問你的網頁并不會看某些路由的內容
使用react-loadable,支持SSR,非常推薦,官方的lazy不支持SSR,這是一個遺憾,這里需要配合wepback4的optimization配置,進行代碼分割
Tips:這里需要下載支持動態import的babel預設包 @babel/plugin-syntax-dynamic-import ,它支持動態倒入組件
webpack配置: optimization: { runtimeChunk: true, splitChunks: { chunks: "all" } }
import React from "react" import Loading from "./loading-window"http://占位的那個組件,初始加載 import Loadable from "react-loadable" const LoadableComponent = Loadable({ loader: () => import("./sessionWindow"),//真正需要加載的組件 loading: Loading, }); export default LoadableComponent
好了,現在路由懶加載組件以及代碼分割已經做好了,而且它支持SSR。非常棒
由于純CSR的網頁一般不是很復雜,這里再介紹一個方面,那就是,能不用redux,dva等集中狀態管理的狀態就不上狀態樹,實踐證明,頻繁更新狀態樹對用戶體驗來說是影響非常大的。這個異步的過程,更耗時。遠不如支持通過props等方式進行組件間通信,原則上除了很多組件共享的數據才上狀態樹,否則都采用其他方式進行通信。
SSR,服務端渲染:這里也使用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 })
const PrerenderSPAPlugin = require("prerender-spa-plugin") new PrerenderSPAPlugin({ routes: ["/","/home","/shop"], staticDir: resolve(__dirname, "../dist"), }),
我覺得掘金上的神三元那篇文章就寫得很好,后面我自己去逐步實現了一次,感覺對SSR對理解更為透徹,加上本來就每天在寫Node.js,還會一點Next,Nuxt,服務端渲染,覺得大同小異。
服務端渲染本質,在服務端把代碼運行一次,將數據提前請求回來,返回運行后的html文件,客戶端接到文件后,拉取js代碼,代碼注水,然后顯示,脫水,js接管頁面。
同構直出代碼,可以大大降低首屏渲染時間,經過實踐,根據不同的內容和配置可以縮短40%-65%時間,但是服務端渲染會給服務器帶來壓力,所以折中根據情況使用。
以下是一個最簡單的服務端渲染,服務端直接吐拼接后的html結構字符串:
var express = require("express") var app = express() app.get("/", (req, res) => { res.send( `hello hello world
` ) }) app.listen(3000, () => { if(!err)=>{ console.log("3000監聽")? } })
只要客戶端訪問localhost:3000就可以拿到數據頁面訪問
//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(); return ` {renderRoutes(routers)} ssr123 ${content}`; };
數據注水,脫水,保持客戶端和服務端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(); client.js {renderRoutes(routers)} {renderRoutes(routers)}
后續可能有利用loader進行CSS的服務端渲染以及helmet的動態meta, title標簽進行SEO優化等,今天時間緊促,就不繼續寫SSR了。
構建Electron極度復雜,超大數據的應用。第一個提到的是sqlite,嵌入式關系型數據庫,輕量型無入侵性,標準的sql語句,這里不做過多介紹。
PWA,漸進性式web應用,這里使用webpack4的插件,進行快速使用,對于一些數據內容不需要存儲數據庫的,但是卻想要一次拉取,多次復用,那么可以使用這個配置
serverce work也有它的一套生命周期通常我們如果要使用 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"//這里配置表示占位符的樣式~。 記得在移動端的滑動屏幕或者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大數據React渲染,擁有讓應用擁有60FPS -非常核心的一點優化this component is lazyloaded by default!; } }
List長列表
]
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個元素;它們當中任意一個的值發生改變時,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.js和javaScript都不適合做大量計算工作,這點有目共睹,尤其是js引擎和GUI渲染線程互斥的情況存在。充分合理利用React的Feber架構diff算法優化項目
requestAnimationFrame調用高優先級任務,中斷調度階段的遍歷,由于React的新版本調度階段是擁有三根指針的可中斷的鏈表遍歷,所以這樣既不影響下面的遍歷,也不影響用戶交互等行為。
使用requestAnimationFrame,當頁面處于未激活的狀態下,該頁面的屏幕刷新任務會被系統暫停,由于requestAnimationFrame保持和屏幕刷新同步執行,所以也會被暫停。當頁面被激活時,動畫從上次停留的地方繼續執行,節約 CPU 開銷。
一個刷新間隔內函數執行多次時沒有意義的,因為顯示器每 16.7ms 刷新一次,多次繪制并不會在屏幕上體現出來
在高頻事件(resize,scroll等)中,使用requestAnimationFrame可以防止在一個刷新間隔內發生多次函數執行,這樣保證了流暢性,也節省了函數執行的開銷
某些情況下可以直接使用requestAnimationFrame替代 Throttle 函數,都是限制回調函數執行的頻率
使用requestAnimationFrame也可以更好的讓瀏覽器保持60幀的動畫
requestIdleCallback,這個API目前兼容性不太好,但是在Electron開發中,可以使用,兩者還是有區別的,而且這兩個api用好了可以解決很多復雜情況下的問題~。當然你也可以用上面的api封裝這個api,也并不是很復雜。
當關注用戶體驗,不希望因為一些不重要的任務(如統計上報)導致用戶感覺到卡頓的話,就應該考慮使用requestIdleCallback。因為requestIdleCallback回調的執行的前提條件是當前瀏覽器處于空閑狀態。
圖中一幀包含了用戶的交互、js的執行、以及requestAnimationFrame的調用,布局計算以及頁面的重繪等工作。
假如某一幀里面要執行的任務不多,在不到16ms(1000/60)的時間內就完成了上述任務的話,那么這一幀就會有一定的空閑時間,這段時間就恰好可以用來執行requestIdleCallback的回調,如下圖所示:
使用preload,prefetch,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文件延遲加載~
普通的腳本
給script標簽,加上async標簽,遇到此標簽,先去請求,但是不阻塞解析html等文件~,請求回來就立馬加載
給script標簽,加上defer標簽,延遲加載,但是必須在所有腳本加載完畢后才會加載它,但是這個標簽有bug,不確定能否準時加載。一般只給一個
寫這篇時間太耗時間,而且論壇的在線編輯器到了內容很多的時候,非常卡,React-native的以及一些細節,后面再補充下面給出一些源碼和資料地址:
手寫React優化腳手架帶項目
react-ssr的源碼
手寫Node.js原生靜態資源服務器
跨平臺Electron的demo
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/116124.html
摘要:往往純的單頁面應用一般不會太復雜,所以這里不引入和等等,在后面復雜的跨平臺應用中我會將那些技術一擁而上。構建極度復雜,超大數據的應用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React為了大型應用而生,Electron和React-native賦予了它構建移動端跨平臺App和桌面應用的能力,Taro則賦...
摘要:往往純的單頁面應用一般不會太復雜,所以這里不引入和等等,在后面復雜的跨平臺應用中我會將那些技術一擁而上。構建極度復雜,超大數據的應用。 showImg(https://segmentfault.com/img/bVbvphv?w=1328&h=768); React為了大型應用而生,Electron和React-native賦予了它構建移動端跨平臺App和桌面應用的能力,Taro則賦...
showImg(https://segmentfault.com/img/bVbw3tK?w=1240&h=827); 前端工程師這個崗位,真的是反人性的 我們來思考一個問題: 一個6年左右經驗的前端工程師: 前面兩年在用jQuery 期間一直在用React-native(一步一步踩坑過來的那種) 最近兩年還在寫微信小程序 下面一個2年經驗的前端工程師: 并不會跨平臺技術,他的兩年工作都是Reac...
摘要:目前我們的業務項目采用的來進行優化和首屏性能提升。可變性需要讓開發人員降低開發時的基準線,來保證每一個用戶的體驗。對于路由的切分以及庫的引入來說,這一個原則至關重要。快速生成一份站點的性能審查報告。 The Cost Of JavaScript 2018 關于原文 原文是在Medium上面看到的,Chrome工程師Addy Osmani發布的一篇文章,這位的Medium上面的自我介紹里...
閱讀 2632·2021-10-14 09:47
閱讀 4909·2021-09-22 15:52
閱讀 3355·2019-08-30 15:53
閱讀 1428·2019-08-30 15:44
閱讀 669·2019-08-29 16:41
閱讀 1646·2019-08-29 16:28
閱讀 438·2019-08-29 15:23
閱讀 1618·2019-08-26 12:20