摘要:之前分享過幾篇關于技術棧的原創文章解析前端架構學習復雜場景數據設計干貨總結打造單頁應用一個項目理解最前沿技術棧真諦一個工程實例今天進一步剖析一個實際案例移動網頁版。目前面臨的問題在于提高產品的各方面性能體驗。
之前分享過幾篇關于React技術棧的原創文章:
解析Twitter前端架構 學習復雜場景數據設計
React Conf 2017 干貨總結1: React + ES next = ?
React+Redux打造“NEWS EARLY”單頁應用 一個項目理解最前沿技術棧真諦
一個react+redux工程實例
......
今天進一步剖析一個實際案例:Uber APP 移動網頁版。
如果你對React技術棧沒有多大興趣,或者不是很了解,也沒有關系。因為讀下來,你會發現,這篇文章的真諦其實在于性能優化上。
本文靈感和主體內容翻譯自Narendra N Shetty的文章:How I built a super fast Uber clone for mobile web,同時進行了大量擴充以及深挖。
出發點和產品雛形很早以來,相信大家都會認同一個觀點:移動端流量超越PC端是不爭的事實。對于前端開發者來說,移動端web的開發同樣非常有趣,也充滿挑戰。
這不,Uber最近發布了最新版本APP,全新樣式,體驗超棒。于是,筆者決定使用React來從零開始構建一個新的屬于自己的Uber。
開發期間,筆者花費了很多時間在基礎組件和樣式搭建上。這環節中,主要應用了Uber官方開放的React地圖庫,并在地圖上“目的地”和“起始點”之間采用svg-overlay和html-overlay去繪制路線。
最終的基本交互可以參考下面Gif圖:
走上優化之路現在,我們有基本的產品形態了。目前面臨的問題在于提高產品的各方面性能體驗。我使用了Chrome Lighthouse去檢驗產品的性能表現。最終得到的結果為:
wow...
第一次繪制時間就已經接近2秒,后面的時間慘不忍睹就不要看了吧。
想象一下,一個用戶拿出手機,企圖叫車。主屏時間的繪制就超過了19189.9ms,這是極其不能忍受的。
接下來,什么也不說了,擼起袖子,想辦法去優化吧。
優化方法1-代碼分離(Code Splitting)我最開始想到并使用的方法就是:Code Splitting(代碼分離),正好我們可以借助webpack來實現這項技術。
什么是webpack code splitting呢? 您可以參考這里,如果英語閱讀吃力,可以參考下面引文:
code splitting就是指將文件分割為塊(chunk),webpack使我們可以定義一些分割點(split point),根據這些分割點對文件進行分塊,并實現按需加載。
因為筆者使用了React技術棧,并采用了react-router,所以代碼的劃分(split)就可以按照路由和加載時機進行。具體操作可以使用react-router的getComponent api來實現:
{ require.ensure([], (require) => { cb(null, require("../components/Home").default); }, "HomeView"); }}>
只有當對應路由被請求時,相應的組件才會被加載呈現。
同時,筆者使用了webpack的CommonChunkPlugin插件提取第三方代碼。這是出于什么考慮呢?
細心的讀者可能會發現上面的code splitting也許會存在一個問題:
按需(按路由)引入資源后,這些資源可能存在大量重復代碼。尤其是我們使用的第三方資源。
想明白這個問題,這時候,你應該就會明白CommonChunkPlugin這個插件的意義了。關于這個插件配置方法有多種,這里我們采用了:有選擇性的提取(對象方式傳參):
{ "entry": { "app": "./src/index.js", "vendor": [ "react", "react-redux", "redux", "react-router", "redux-thunk" ] }, "output": { "path": path.resolve(__dirname, "./dist"), "publicPath": "/", "filename": "static/js/[name].[hash].js", "chunkFilename": "static/js/[name].[hash].js" }, "plugins": [ new webpack.optimize.CommonsChunkPlugin({ name: ["vendor"], // 公共塊的塊名稱 minChunks: Infinity, // 最小被引用次數,最小是2。傳遞Infinity只是創建公共塊,但不移動模塊。 filename: "static/js/[name].[hash].js", // 公共塊的文件名 }), ] }
這樣子,我們把公共代碼(react、react-redux、redux、react-router、redux-thunk)專門抽取到vendor模塊中。
通過上述方法,筆者欣喜地發現:
First meaningful paint時間由19189.9ms縮短到4584.3ms:
這無疑是激動人心的。
優化方法2-Server side rendering(服務端直出)也許你一直在聽說過“服務端渲染”或者“服務端直出”這樣的名詞。但是從未實踐過,也從來沒有了解過他的意義。好吧,這里我先描述一下,到底什么是服務端直出。
服務端直出,其實簡單總結為服務器在接到來自瀏覽器第一次請求時,便返回一個“初步最終”HTML文檔。這個HTML文檔已經進行了數據拼接。這樣用戶能以最快的時間看到首屏的效果,當然這個效果是“閹割版”的,非最終版本。
這種方式主要是針對“前后分離”的傳統模式。傳統模式中,服務器返回HTML文檔,之后瀏覽器解析文檔標簽,拉取CSS,之后拉取JS文件。JS文件加載完成之后,執行JS內容,并發送請求獲取數據。最終,將數據渲染在頁面上。
由此,Server side rendering方式將JS請求數據的過程放在了服務器上,甚至對于數據與HTML結合處理也可以在服務器上做。
這樣一來,主要就是加快了首屏渲染時間。當然,使用服務端渲染,還能夠優化前端渲染難以克服的SEO問題。
理論理解起來很簡單,難處就在于服務器端環境的前端腳本如何處理,如何與客戶端保持一致。
在這個項目中,我使用了Express作為nodeJS框架,結合react-router完成:
server.use((req, res)=> { match({ "routes": routes, "location": req.url }, (error, redirectLocation, renderProps) => { if (error) { res.status(500).send(error.message); } else if (redirectLocation) { res.redirect(302, redirectLocation.pathname + redirectLocation.search); } else if (renderProps) { // Create a new Redux store instance const store = configureStore(); // Render the component to a string const html = renderToString(); const preloadedState = store.getState(); fs.readFile("./dist/index.html", "utf8", function (err, file) { if (err) { return console.log(err); } let document = file.replace(//, ` ${html}`); document = document.replace(/"preloadedState"/, `"${JSON.stringify(preloadedState)}"`); res.setHeader("Cache-Control", "public, max-age=31536000"); res.setHeader("Expires", new Date(Date.now() + 2592000000).toUTCString()); res.send(document); }); } else { res.status(404).send("Not found") } }); });
通過上述方法,我們欣喜地發現:
First meaningful paint時間已經縮短到921.5ms:
這無疑是令人振奮的。
優化方法3-Compressed static assets(壓縮靜態文件)壓縮文件,當然是一個容易想到而且行之有效的措施。為此,我使用了webpack的CompressionPlugin插件:
{ "plugins": [ new CompressionPlugin({ test: /.js$|.css$|.html$/ }) ] }
同時,使用express-static-gzip來對服務端進行配置:
server.use("/static", expressStaticGzip("./dist/static", { "maxAge": 31536000, setHeaders: function(res, path, stat) { res.setHeader("Expires", new Date(Date.now() + 2592000000).toUTCString()); return res; } }));
express-static-gzip是一個處于express.static之上的中間件。如果對于指定路徑的文件沒有找到壓縮版本,就使用為壓縮版本進行返回。
經過此處理,我們縮短了400ms時間,OK,現在First meaningful paint時間為546.6ms.
優化方法4-Caching(緩存)截止到此,我們已經從最初的19189.9ms已經優化到546ms,我們當然繼續可以在客戶端進行靜態文件緩存來使得加載時間變得更短。
筆者使用了sw-toolbox搭配service workers進行。
sw-toolbox:A collection of service worker tools for offlining runtime requests.
Service Worker Toolbox provides some simple helpers for use in creating your own service workers. Specifically, it provides common caching strategies for dynamic content, such as API calls, third-party resources, and large or infrequently used local resources that you don"t want precached.
簡單翻譯下:
Service Worker實現常見運行時緩存模式,例如動態內容、API調用以及第三方資源,實現方法就像編寫README一樣簡單。
也許到這里你一頭霧水,沒關系,我們從最初開始,了解一下什么是service worker:
在2014年,W3C公布了service worker的草案,service worker提供了很多新的能力,使得web app擁有與native app相同的離線體驗、消息推送體驗。
service worker是一段腳本,與web worker一樣,也是在后臺運行。
作為一個獨立的線程,運行環境與普通腳本不同,所以不能直接參與web交互行為。native app可以做到離線使用、消息推送、后臺自動更新,service worker的出現是正是為了使得web app也可以具有類似的能力。
而sw-toolbox,顧名思義,就是service worker一個toolbox,具體我們看代碼:
toolbox.router.get("(.*).js", toolbox.fastest, { "origin":/.herokuapp.com|localhost|maps.googleapis.com/, "mode":"cors", "cache": { "name": `js-assets-${VERSION}`, "maxEntries": 50, "maxAgeSeconds": 2592e3 } });
上面代碼的意思是,我們對于get類型的請求,當請求內容為js腳本時,應用toolbox.fastest handler處理。
toolbox.fastest指示:對于這個請求,我們既從緩存中獲取,也同時通過正常的請求network獲取。這兩種方式哪個返回快,就應用哪一個。
另外,toolbox.router.get的第三個參數表示配置項。
考慮周到的讀者可能會想,上面是對于支持Service worker的瀏覽器,那么對于不支持的瀏覽器呢?我們干脆設置:
res.setHeader("Expires", new Date(Date.now() + 2592000000).toUTCString());
通過這樣處理,我們來直觀感受一下頁面加載瀑布流:
優化方法5-Preload and then load(預加載/延后加載)如果你還沒聽說過“Preload”,不要緊。我們這就來了解一下:
Preload作為一個新的web標準,旨在提高性能和為web開發人員提供更細粒度的加載控制。Preload使開發者能夠自定義資源的加載邏輯,且無需忍受基于腳本的資源加載器帶來的性能損失。
換成你能聽明白的話來說:
preload建議允許始終預加載某些資源,瀏覽器必須請求preload標記的資源。
這樣子,究竟有什么意義呢?
舉個例子:比如一些隱藏在CSS和Javascript中的資源。
當瀏覽器發現自己需要這些資源時已經為時已晚,所以大多數情況,這些資源的加載都會對頁面渲染造成延遲。
preload的出現就是為了優化這個過程。
對于preload的兼容性,可以參考這里。
對于不支持preload的瀏覽器,筆者使用了prefetch來處理。
但于preload不同,prefetch的作用是告訴瀏覽器加載下一頁面可能會用到的資源,注意,是下一頁面,而不是當前頁面。因此該方法的加載優先級非常低。
這些新標準其實很有意思,里面的內容遠不止這些。有興趣的同學可以自行了解,也歡迎與我討論。
回到正題,我在head標簽中使用:
最終優化的結果如圖:
總結其實,使用React+Webpack做出一個Uber已經不是重點了。真正激動人心的是整套流程的優化之路。我們使用了大量成熟的、未成熟(新技術),希望對讀者有所啟發!
Happy Coding!
PS: 作者Github倉庫,歡迎通過代碼各種形式交流。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91700.html
摘要:在,是當之無愧的王者,贏得了與之間的戰爭,攻陷了的城池。于月發布了版本,這一版本為了更好的表現加入了渲染方式。前端框架這個前端框架清單可能是年疲勞的元兇之一。的創建者,目前在工作為尋找構建簡單性和自主配置性之間的平衡做了很大的貢獻。 春節后的第一篇就從這個開始吧~本文已在前端早讀課公眾號上首發 原文鏈接 JavasScript社區在創新的道路上開足了馬力,曾經流行過的也許一個月之后就過...
摘要:相關狀態父組件傳遞給子組件的狀態。外部狀態狀態是可以從視圖庫中移出來的,然后可以使用提供者消費者模式把狀態重新連接回視圖庫。重新設計在我看來,重寫是有其必要性的,至少有以下個方面可以改進得更友好。 Redux 學習起來很困難?寫起代碼來很啰嗦?一起來看看 Rematch 的作者對 Redux 的思考和簡化吧~ 原文:《Redesigning Redux》, Shawn McKay 都過...
摘要:多端統一開發框架優秀學習資源匯總官方資源項目倉庫官方文檔項目倉庫官方文檔微信小程序官方文檔百度智能小程序官方文檔支付寶小程序官方文檔字節跳動小程序官方文檔文章教程不敢閱讀包源碼帶你揭秘背后的哲學從到構建適配不同端微信小程序等的應用小程序最 Awesome Taro 多端統一開發框架 Taro 優秀學習資源匯總 showImg(https://segmentfault.com/img/r...
摘要:總結本文分析了在采用架構下的數據設計結構,在一個復雜的場景下,希望引起讀者對能有一個更深入的認識。 前幾天刷Twitter,發現Nicolas(Engineering at @twitter. Technical Lead for Twitter Lite)發布了這么一條推文: showImg(https://segmentfault.com/img/remote/1460000009...
閱讀 3376·2021-11-22 13:53
閱讀 3411·2021-10-11 11:11
閱讀 932·2019-08-30 14:12
閱讀 1222·2019-08-29 17:16
閱讀 640·2019-08-29 16:45
閱讀 3349·2019-08-29 12:56
閱讀 670·2019-08-28 17:55
閱讀 2065·2019-08-26 13:24