天天看點

落地頁性能進化之路

作者:閃念基因

資訊流投放是公司擷取公域流量的關鍵管道,也是公司外部流量的主要來源。而投放落地頁會作為這個流量鍊路的第一步最先映入使用者眼簾。是以落地頁整體的加載體驗和互動體驗變成了影響使用者轉化的重要因素。

摘自Google對于“核心網頁名額”的報告:

為什麼網頁性能很重要

研究表明,更好的核心網頁名額可以提高使用者互動度和業務名額。例如:

研究表明,當網站達到核心網頁名額門檻值時,使用者放棄加載網頁的可能性會降低 24%。

Largest Contentful Paint (LCP) 每減少 100 毫秒,Farfetch 的網站轉化率就會提高 1.3%。

我們目前的衆多落地頁都是由低代碼平台搭建出來的,在前期投放過程中暴露出了許多性能及互動問題,譬如首頁白屏、異步元件加載耗時等。

從兩個比較關鍵的加載性能名額來看,目前頁面的中位數FCP超過1500ms,中位數LCP超過4000ms,而對比業界标準,達到優秀級别的資料是FCP低于1000ms,LCP低于2500ms,顯然有着不小的差距。那針對落地頁,我們也會以最低達到這個标準為目标去拆解優化,同時在頁面内的互動上也需要優化細節。

下面就來分享下整個優化動作是如何進行的。

拆解分析

一個性能表現不佳的Web頁面,通過對Lighthouse及Performance的分析,結合加載時序圖,已經可以找出90%的問題了,這裡我們先來看一張對應目前頁面的簡易時序圖:

落地頁性能進化之路

結合整個項目代碼來分析(具體的代碼邏輯比較分散,這裡就不貼出來了),大緻總結為如下六個問題點:

1、針對DNS、TCP以及TLS連接配接,目前代碼無任何優化邏輯,這一塊兒大有可為;

2、JS/CSS靜态資源檔案存在單個檔案過大的問題(Gzip前超過500KB);

3、靜态資源包存在重複引用問題;

4、在觸發FCP及LCP之前是否可以避免XHR請求的阻塞?

5、FCP完全可以在HTML加載完畢後就觸發,而不應該在靜态資源及XHR請求之後;

6、圖檔及元件仍有優化空間:異步元件、圖檔壓縮并轉為webp、圖檔預加載。

以正常思路針對每個具體的問題去做分析可能會得到以下辦法,比如:

1、通過SSR/SSG來解決XHR請求阻塞和靜态資源加載阻塞問題;

2、通過依賴分析及合理分包方案來解決靜态資源檔案問題;

3、通過預連接配接解決網絡連接配接耗時;

4、通過預加載解決異步元件以及大圖檔的加載耗時。

實際優化過程:

除了重構為SSR外,其他方案相對來說成本更低,也更易實施,于是乎我們先應用相對更正常的方案,吭哧吭哧優化完畢後,加載資料表現也基本快達到我們設立的目标了,但是XHR請求和靜态檔案的阻塞仍然沒有解決,頁面還存在白屏,使用者實際體感上還是有缺陷,最後就不得不考慮令人頭痛的SSR重構了......

接下來針對上述問題,我這裡來逐一拆解并詳細分析,看看最後是如何達到滿意的效果的。

工程建構

這裡主要是為了解決拆解分析中提到的第2點、第3點。

1、依賴分析

這部分的第一件事情就是打開依賴分析,每次有比較大的變動都先在開發環境看看依賴分析,加載進來的東西是否合理,做到心中有數。

不管是Vite還是Webpack,都有對應的工具,比如Webpack的webpack-bundle-analyzer可視化工具,通過可視化界面以及生成的stats.json,我們就可以進行逐一分析和優化了。

落地頁性能進化之路

2、重複依賴

這裡落地頁通過上面的依賴分析就發現了一個很明顯的問題:

低代碼平台元件庫的一些元件的dependencies都寫了相同的依賴包,導緻npm i的時候,這些依賴被重複安裝并且被重複打包:

落地頁性能進化之路

最簡單的方法就是去掉子元件的dependencies,将依賴寫在最外層,當然也有更優雅的方法去處理,這裡處理掉重複依賴後,生産環境的靜态檔案少了100KB+。

這裡隻是其中的一個執行個體,借助這個工具,同學們也能更輕松的發現一些其他的未知問題。

3、合理分包

關于分包,建構工具會有自己的一套預設政策,但是如果想做到極緻,必然是需要針對具體的業務場景做定制化配置的。

針對落地頁,我制定了一個分包原則,然後根據這個原則去做細化配置:

前端架構- UI庫 - 核心工具庫 - 同步元件 - 異步元件 - 跟随預設政策

詳細的配置這裡就不貼出來了,以Webpack為例就是利用splitChunks配合正則提前配置好分割參數,而其中異步元件的分割則是配合import()方法來實作的,這一塊兒在後面會有詳細的介紹。

下圖是做了細化的分包後,npm run build的靜态檔案清單:

落地頁性能進化之路

實際上這個清單中的絕大部分檔案都是根據頁面互動按需加載的(也就是上面提到的異步元件分割),保證了在首頁隻加載必要内容。同時之前存在的單個大檔案(單檔案超500KB)也被拆分為多個小檔案,圖中可以看到Gzip後,最大的檔案不超過100KB。

根據浏覽器的6個TCP連接配接并發限制,在HTTP1.1時代你可能覺得這是個負優化,因為那時候還流行CSS雪碧圖呢,也是為了降低并發數。但是HTTP2.0的優勢在這種情況下卻可以得到比較好的發揮,從落地頁這邊實際實驗對比下來,正常網絡環境加載單個大檔案(800KB左右)比加載5個小檔案要慢5%左右,如果是弱網環境,差距會更明顯一些。

更重要的,單個大檔案不利于緩存管理,一些常年不咋動的子產品,比如上面分包原則中的前端架構 、UI庫 、核心工具庫 ,這些都是屬于疊代頻率很低的,單獨拆出能有效利用緩存。

如果有同學覺得自己的頁面加載表現不佳,同樣可以嘗試借鑒上面的思路去做優化。

預連接配接、預加載

拆解分析的第1點、第6點都會在下面的動作中得以優化。

1、預連接配接

預連接配接顧名思義就是預先建立網絡連接配接,這其中包括DNS、TCP、TLS,當後續請求該域名資源時,可直接使用已建立好的連接配接。是以需要在比較關鍵的節點上使用,比如:JS檔案域名、主圖檔域名、第三方重要的SDK。

目前可用方法為:

  • dns-prefetch,預先對指定域名進行DNS解析。
  • preconnect,預先建立所有連接配接,包括DNS、TCP、TLS。

這兩個都是HTML特性,落地頁這邊是兩個一起配合使用,在preconnect不支援的時候,以dns-prefetch來提供降級:

落地頁性能進化之路
落地頁性能進化之路

在上圖落地頁實際Connection View的表現中,可以很直覺的看到,在preconnect的作用下,實際資源請求之前,TCP和TLS連接配接已經被預先建立了。

這塊兒的時間會被直接節省掉,這還是在有DNS緩存的情況下,如果是新使用者沒有DNS緩存,那由于DNS延遲帶來的時間節省将會更多。

2、預加載

預連接配接是建立連接配接但不加載,而預加載是加載但不執行,目前主要用到的是prefetch和preload特性。

2.1、prefetch、preload

- preload提示浏覽器以更高的優先級立即加載靜态資源并緩存,以期在後續使用時候無需等待,在浏覽器中的Priority辨別為High

- prefetch提示浏覽器未來可能會使用到的某個資源,浏覽器就會在閑時去加載對應資源并緩存,在浏覽器中的Priority辨別為Lowest,是優先級最低的

可見preload适合馬上需要用到的資源,或者說是目前頁面中的重要資源,以此來提高它們的加載優先級,落地頁中,在body标簽之前,我們将首頁需要的JS檔案都設定了preload,這樣在body标簽開始渲染之前,JS已經開始加載了,同時不阻塞後續标簽的解析執行,有點類似于script标簽的defer屬性。

落地頁性能進化之路
落地頁性能進化之路

這裡對一張主背景圖使用了preload,并且通過fetchPriority将其優先級調整到了High,下面這張圖就可以看出使用和不使用“提取優先級”的差別,使用preload并且将優先級設定為High後,LCP 時間從 2.6 秒縮短到 1.9 秒。

落地頁性能進化之路

上面講的都是關于preload的,那prefetch的應用場景在哪裡?來看看它和異步元件的配合吧。

2.2、如何将預加載應用到異步元件

落地頁性能進化之路

首先該元件會在打包時被分割為單獨的名為“CmsLayerGradeTabSelector”的JS檔案。

上面的webpackPrefetch注釋會被webpack翻譯為對應的rel="prefetch"的link标簽。也就意味着該檔案會被浏覽器在空閑時加載并緩存,當互動觸發異步元件的時候,直接去緩存中取檔案。

如果不加webpackPrefetch,JS檔案會按需加載,也就是互動觸發的時候才加載,進而帶來的就是互動體驗問題了。

2.3、精确控制加載時機

落地頁性能進化之路

通過上面的代碼,在實際場景中,我們可以任意時刻控制某個資源的預加載和執行。比如你知道接下來将會播放一個視訊、渲染一張大圖,都可以預先加載它們。

由低代碼平台引出的問題

上述步驟全部優化完畢後,整體效果提升也比較明顯,測得線上的FCP和LCP已經比較接近設立的目标了:FCP低于1000ms,LCP低于2500ms,但是由于XHR和靜态資源的阻塞仍未解決,在文章開篇也提到了,這裡會導緻白屏,使用者體感不佳,如果能解決這個,那整體表現會再上一個台階。

最後針對第4點、第5點,我們發現問題其實是由低代碼平台引出的,低代碼平台在出碼後,直接将内容存到後端,前端需要根據對應頁面ID去即時擷取頁面内容以及業務資料。這樣會直接導緻首頁的白屏,因為即使是在靜态資源完全加載完畢後,仍然需要調用接口擷取頁面資料,而後才能開始渲染頁面。

分析下來,其實這裡如果能做到頁面資料的靜态化而非即時拉取,就可以完全解決問題。

SSR

最先想到的可能是将頁面由CSR重構為SSR,可以完全解決掉靜态資源以及XHR請求問題,但是因為目前頁面依托于低代碼平台,且已經疊代了多次。重構會涉及到整個B端,低代碼平台邏輯複雜,評估下來的開發成本很高。同時大流量的投放頁面,采用SSR對伺服器要求很高,可能需要大量堆疊機器才能保證性能,這塊兒又會增加支出。

資料靜态化

退一步想,結合SSR的優點,如果能将XHR請求相關的頁面資料預先放在HTML裡面,通過HTML直出,就能以最快速度拿到頁面資料及結構了,相比SSR來說,就隻多了靜态資源的加載,而在靜态資源加載之前,我們同樣也能幹很多事情。

頁面如何靜态化?

1、靜态化改造

一個合格的低代碼平台完全可以做到這一點,首先看一下沒有靜态化的頁面生成流程:

落地頁性能進化之路

從這張圖可以就看出問題了,C端在通過XHR請求拿到JSON Schema的那一刻才能開始渲染動作,這裡需要做的是讓JSON Schema資料以靜态形式存儲在HTML中。在Nginx以及服務端做一些動作可以很好的去實作這個想法:

落地頁性能進化之路

對比起來發現流程複雜了不少:

每次B端釋出一個頁面,服務端會從輕舟容器中取出原始HTML,并且将JSON Schema通過script标簽嵌入到HTML中,随後标記上對應的projectId存儲到OSS中。

C端HTML請求會被Nginx轉發到後端服務,後端取出對應的拼接好的HTML傳回給前端。

落地頁性能進化之路

到了這一步,我們已經做到了資料靜态化,進而節省掉了靜态資源加載、XHR請求擷取JSON Schema這兩個非常耗時的步驟,提前拿到了頁面資料,畫一個簡易的Waterfall來看看差別:

落地頁性能進化之路

靜态化之後,First Contentful Paint在HTML加載完畢後就會發生,而且是一次有效的内容繪制,因為我們已經知曉頁面内容了。

2、提前繪制

接上一節,在知曉頁面結構和資料的情況下,如何才能更快更全的渲染出必要内容,用以留住使用者,現階段我們沒有JS和CSS,是以隻能在Body中寫簡易邏輯來處理JSON Schema,展示出關鍵内容:

落地頁性能進化之路
落地頁性能進化之路

通過上面的處理,借助頁面資料中的base64格式頭圖,幾乎立即就能展示出頁面的頭圖、課程資訊、詳情圖等關鍵内容,這時候使用者已經可以進行浏覽了,剩餘的完整内容隻需要等待靜态檔案的加載就行了。

處理完畢後的頁面表現,清掉緩存,在正常的無線網絡環境下,視覺感受幾乎是秒開:

落地頁性能進化之路

如果想在這個時機展示更多内容,我們也可以自由拓展,這裡是非常靈活的。

到這裡拆解分析中的第5點、第6點就被完全解決了,總結一下:

對于一個成熟的平台,頁面的靜态化處理肯定是必須的,可以是各種其他的處理形式,借鑒SSR、SSG、ISR等各種渲染方案的優勢,來做到最大化貼合自己的業務。

這個思路其實同樣也适用于一些小的站點,因為改造成本其實并不高:利用好OSS、改造一下後端存儲邏輯、增加一個接口即可實作。

最後

落地頁在做完這些優化後,線上中位數FCP從1500ms縮短到了500ms+,LCP從4000ms+縮短到2000ms。

優化完至今已經穩定運作了一段時間,而針對目前現狀仍然還有一個可以優化的點:

目前針對落地頁靜态化後的HTML,由于考慮到頁面版本控制、緩存問題、以及線上的業務對各業務線有不同的域名要求,是由Nginx轉發到後端接口再去OSS擷取的,後續針對一些特殊的對域名要求寬松、變動頻率低的業務場景,可以考慮直接申請CDN域名,通過CDN域名投放。這樣就省掉了Nginx、後端服務的兩層處理帶來的時間損耗。

整篇文章涉及到的大部分優化都是比較通用的手段,能帶來實打實的提升,但在實際開發中經常被很多同學忽略,其實在項目立項之初,就應該提前考慮好大緻的代碼組織架構、可以實作的正常以及非正常優化方案,避免在後期再來分析優化進而帶來更高額的成本。

作者:胡斌

來源-微信公衆号:高途技術

出處:https://mp.weixin.qq.com/s/yjEnfe_RTTAMP2bAaAXLxA