周三下午,我打開Vercel的部署日志,盯著上面的Core Web Vitals評分發(fā)呆。一個內(nèi)容還不錯的項目,LCP(最大內(nèi)容繪制)卻一直在2.8秒徘徊,移動端CLS(累計布局偏移)更是飆到了0.25。對于靠搜索流量吃飯的站點,這點差距就是用戶直接關(guān)掉頁簽和讀完文章的區(qū)別。更要命的是,Google明確把Core Web Vitals當(dāng)作排名信號,而用戶不等頁面畫完就已經(jīng)離開了。那之后,我花了兩個下午,把一份自己反復(fù)使用的性能清單在Next.js構(gòu)建流程里又從頭到尾跑了一遍。效果立竿見影——不是靠換一個更快的服務(wù)器,也不是刪掉什么神奇的功能,就是一串有點無聊但每次都有效的工程習(xí)慣。
下面就是這份清單里真正起作用的部分,按回報最高的順序排列。每一點都不新奇,但合在一起,就能把“看起來挺快”變成“刷一下就出來”。
第一步:給客戶端少送點JavaScript
React站點變慢,絕大多數(shù)時候不是因為React本身渲染慢,而是因為客戶端被灌進(jìn)了太多根本沒用上的代碼。一個簡單的數(shù)據(jù)展示頁面,整整400KB的JavaScript,從第三方包到很久沒點開的模態(tài)框邏輯,全都一口氣塞給瀏覽器。用戶打開頁面只是想看幾行文字,卻要先下載、解析、執(zhí)行一個龐大的SaaS后臺。
我的第一刀總是切在“是不是真的需要客戶端組件”上。在Next.js App Router里,默認(rèn)所有組件都是Server Component,只有真正用到狀態(tài)、事件或者瀏覽器API的部分,才值得加上'use client'標(biāo)記。而且這個標(biāo)記要盡量往下推,別圖省事在頁面頂層就打上客戶端標(biāo)簽,只因為底部有一個需要交互的按鈕。讓客戶端真正負(fù)責(zé)的,就是那一個按鈕自己,而不是整棵組件樹。
對于那些體積大、首屏又看不見的東西——圖表、重型編輯器、3D場景、點擊后才彈出的對話框——next/dynamic是最直接的解藥。我通常這么寫:
const GalaxyScene = dynamic(() => import('@/components/GalaxyScene').then((m) => m.GalaxyScene),{ ssr: false, loading: () => null },
這串代碼把GalaxyScene的加載完全推遲到它真正需要渲染的時候,同時不觸發(fā)服務(wù)端渲染,loading狀態(tài)干脆什么都不顯示,用戶根本沒機(jī)會注意到這個組件是后來才加載出來的。削減JavaScript不是在功能上讓步,而是把有限的網(wǎng)絡(luò)和CPU預(yù)算用在用戶第一眼看到的東西上。第一眼沒看到的東西,完全可以排在后面。
第二步:別讓布局突然跳起來
CLS是Core Web Vitals里最容易破壞用戶感受的指標(biāo),也是最好修復(fù)的。頁面剛出來,用戶正要點擊一個按鈕,廣告加載了,或者一張圖片突然撐開了容器,手指就點到了別的地方。這種體驗比加載慢還要讓人煩躁,因為它打斷了已經(jīng)開始的閱讀或操作。
解決CLS的核心思路很直白:為所有還沒出現(xiàn)的內(nèi)容提前預(yù)留好空間。圖片是最常見的“罪魁禍?zhǔn)住薄ext/image組件只要給它顯式的width和height,瀏覽器就能在渲染之前算出圖片會占據(jù)多少空間,不會等圖片下載完才重新排版。如果想讓它填滿某個容器,就用fill屬性搭配一個明確尺寸的父容器,同樣能達(dá)到預(yù)留空間的效果。
對于廣告、嵌入內(nèi)容、橫幅等按需加載的模塊,哪怕后端接口返回得再快,也要提前在占位元素上設(shè)置好寬高,或者至少給一個合理的min-height。字體加載也會引起類似的問題,本地回退字體和網(wǎng)絡(luò)字體字寬不同,文本就會在切換瞬間抖動一下。用next/font加載字體,并且把display設(shè)置為swap,可以在保證文字立即可見的同時,減少字體交換帶來的重排。比如:
import { Inter } from 'next/font/google';const inter = Inter({ subsets: ['latin'], display: 'swap' });
這樣一來,文字不會因為網(wǎng)絡(luò)字體遲遲不到而空白一片,也不會因為字體度量差異而讓整段文字突然移動。布局穩(wěn)定之后,用戶看到的頁面更像一個印刷好的稿子,而不是還在搭建中的腳手架。
第三步:把圖片當(dāng)成頁面上最重的資源來對待
在很多站點上,圖片占整個頁面體積的60%以上。我們經(jīng)常無意識地讓一張4000像素寬的照片塞在一個400像素的卡片槽里,浪費(fèi)的字節(jié)比整個頁面文字還多出十倍。Next.js內(nèi)置的next/image組件幾乎把圖片優(yōu)化該做的事都打包好了:自動生成AVIF和WebP格式,根據(jù)屏幕實際顯示尺寸提供合適的源文件,默認(rèn)開啟懶加載,只要不用到第一屏關(guān)鍵圖片,它就乖乖地等。
首屏的主視覺圖是個例外。如果這張圖恰好在用戶第一眼看到的地方,懶加載反而會拖慢它出現(xiàn)的時間。這時候一定要加上priority標(biāo)記,告訴Next.js這張圖片需要提前加載,不應(yīng)該被延遲。其余的圖片,包括列表里成排的頭像、文章配圖、圖庫內(nèi)容,全都交給默認(rèn)的懶加載就好。還有一個容易被忽略的細(xì)節(jié):始終確保提供的圖片尺寸跟它在頁面上實際渲染的尺寸接近。這并不需要手動一個個去裁圖,next/image的尺寸控制結(jié)合CDN的圖像處理能力,就能在請求時自動裁出接近顯示區(qū)域的大小,從根本上杜絕浪費(fèi)。
這些做法堆在一起,能直接讓圖片帶來的帶寬消耗降到原來的幾分之一,同時肉眼幾乎看不出畫質(zhì)差異。尤其在移動網(wǎng)絡(luò)下,這種削減就是頁面從等三秒鐘到秒開之間的差距。
第四步:盡可能在服務(wù)端渲染完,并把結(jié)果緩存起來
Next.js給了我們幾種渲染策略,對于不依賴實時用戶數(shù)據(jù)的內(nèi)容,靜態(tài)生成或增量靜態(tài)再生成是最直接有效的選擇。一個預(yù)渲染好的頁面,從CDN邊緣節(jié)點直接推給瀏覽器,幾乎不需要服務(wù)端計算時間,首字節(jié)時間能做到幾十毫秒。與每次請求都現(xiàn)場拼組件、拉數(shù)據(jù)、渲染HTML相比,這個差距不是一點半點。
如果一定要在服務(wù)端取數(shù)據(jù),也別在每個請求上都重新抓一遍。Next.js擴(kuò)展的fetch API支持緩存和重新驗證策略。比如可以設(shè)置revalidate,讓同一個請求在指定時間內(nèi)復(fù)用已緩存的結(jié)果,而不是重復(fù)調(diào)取遠(yuǎn)端API。這在不影響內(nèi)容新鮮度的前提下,能極大減少數(shù)據(jù)層的壓力,也讓后續(xù)請求的響應(yīng)時間幾乎變成常量。
第三方腳本也是個常見的性能陷阱。分析工具、在線客服、視頻嵌入,這類腳本的加載方式如果不加限制,會霸道地?fù)屨贾骶€程,把頁面的可交互時間硬生生往后推。next/script組件的strategy="lazyOnload"可以輕松解決這個問題。把這些不緊急的腳本推遲到頁面上所有關(guān)鍵元素都加載完畢之后才運(yùn)行,主線程就不會在早期被它們堵塞。用戶仍然可以在頁面完全可交互之后繼續(xù)正常使用那些工具,但首頁加載時的體感已經(jīng)不再是他們埋單。
第五步:在真正用戶的設(shè)備上測量,別被自己的開發(fā)機(jī)騙了
光纖直連、i7處理器、32GB內(nèi)存的開發(fā)機(jī)器上跑Lighthouse,分?jǐn)?shù)漂亮得不像話,但這跟真實用戶隔著層層移動信號看到的東西完全是兩個世界。我強(qiáng)制自己養(yǎng)成一個習(xí)慣:只在模擬慢速移動設(shè)備的Lighthouse模式里看分?jǐn)?shù),而且更多是參考Search Console里基于真實用戶的CrUX數(shù)據(jù)。Google的排名看的是真實用戶數(shù)據(jù),不是自己本地跑出來的實驗室數(shù)據(jù)。
還有一個讓團(tuán)隊持續(xù)受益的做法是設(shè)定一個性能預(yù)算,比如首次加載JavaScript總量不超過150KB。把這個限制加到CI流程里,一旦某個PR讓它超標(biāo),構(gòu)建直接報錯。這種硬約束把速度退化擋在合并代碼之前,也讓所有人從最開始就意識到自己引入的每一個依賴都在預(yù)算里記賬。看起來嚴(yán)格,實際效果卻像是自動剎車,把緩慢降級扼殺在搖籃里。
到這里會發(fā)現(xiàn),這些動作沒有一個是需要額外學(xué)習(xí)什么革命性技術(shù)。server組件、動態(tài)導(dǎo)入、圖片優(yōu)化、緩存策略、預(yù)算監(jiān)控,都是Next.js生態(tài)里已經(jīng)準(zhǔn)備好的工具。真正能帶來差異的,是執(zhí)行這些“無聊的事”的連貫性。一個頁面走完這五步,可能看不出任何花哨的地方,但那種一觸即達(dá)的加載感,會用評論區(qū)更長的停留時間、更低的跳出率,以及逐漸上漲的自然搜索流量,默默兌現(xiàn)回報。
對我自己來說,這份清單已經(jīng)變成了每次做Next.js項目時的默認(rèn)配置——不是等到性能報警了再沖去救火,而是從一開始就把速度寫進(jìn)構(gòu)建流水線里。后來看到別人在同樣的問題上反復(fù)踩坑,我才把這些步驟總結(jié)成可復(fù)用的檢查項。如果你也在琢磨自己的Next.js項目為什么沒有想象中那么快,可以先別急著研究什么黑科技,把這幾步老老實實走一遍,也許答案就已經(jīng)出來了。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.