前端性能優(yōu)化 之 首屏預(yù)渲染

本文介紹一種優(yōu)化首屏訪問速度的技術(shù):前端預(yù)渲染,并封裝自定義的React hook,解決預(yù)渲染中的數(shù)據(jù)初始化問題。

一、首屏速度慢的問題

如果網(wǎng)站頁面首屏訪問比較慢,應(yīng)該怎么優(yōu)化呢?

這要結(jié)合實際情況,有非常多的手段可以提升首屏訪問速度,今天我們來聊一聊其中一種技術(shù):前端預(yù)渲染。

先上效果圖

image.png
image.png

上面是一個前端刷題網(wǎng)站:靈題庫(https://www.lingtiku.com,一個前端面試刷題題庫,收集大廠真題)??梢钥吹絻?yōu)化之前,頁面要在“題庫加載中”狀態(tài)停留幾秒,才能獲取到數(shù)據(jù),渲染出題集列表。優(yōu)化之后秒開。

靈題庫的首屏展示的題集列表依賴一個列表接口,這個接口很慢(接口慢是因為API項目部署在阿里云的函數(shù)計算上,彈性實例啟動有預(yù)熱過程,用常駐實例又要額外付費,serverless內(nèi)容后面會專門介紹),每次訪問都要loading很久。怎么辦呢?

因為這個首屏的列表數(shù)據(jù)不經(jīng)常改動,所以我考慮可以在構(gòu)建階段就請求好數(shù)據(jù),并渲染成靜態(tài)頁面,這就是前端預(yù)渲染的基本思路。

二、前端預(yù)渲染工具的原理和使用

萬能的npm提供了前端預(yù)渲染工具:

靈題庫網(wǎng)站使用的是rollup的插件。

我們先來了解下預(yù)渲染工具的原理,然后再介紹具體的用法。

image.png

預(yù)渲染工具的基本原理是:構(gòu)建打包之后,插件會在本地啟動express靜態(tài)服務(wù),serve打包好的靜態(tài)資源。然后再啟動一個無頭瀏覽器(例如Puppeteer),瀏覽器從服務(wù)器請求網(wǎng)頁,網(wǎng)頁運行時候會請求首屏接口,用拿到的數(shù)據(jù)渲染出包含內(nèi)容的首屏后,無頭瀏覽器截屏并替換掉原來的html。

基本使用示例:使用很簡單,配置插件并提供必要參數(shù)即可,需要指定打包文件的輸出目錄和需要渲染的路由:

// webpack.config.js
const path = require('path');
const PrerenderSPAPlugin = require('prerender-spa-plugin');

module.exports = {
  plugins: [
    // ... other config
    new PrerenderSPAPlugin({
      // required,打包的文件輸出目錄,預(yù)渲染工具會在這個目錄啟動express服務(wù)
      staticDir: path.join(__dirname, 'dist'),
      // required,指定需要預(yù)渲染的路由
      routes: [ '/', '/about', '/some/deep/nested/route' ]
    })
  ]
};

高級用法:這里說兩個常用的選項renderer.renderAfterTimerenderer.headless

renderer選項讓用戶可以自定義渲染器,即用來加載頁面并截圖生成HTML的無頭瀏覽器。一般我們選擇插件內(nèi)置的PuppeteerRenderer

renderer.renderAfterTime可以讓控制頁面加載好后等一段時間再截圖,保證數(shù)據(jù)已經(jīng)都拿到,頁面渲染完畢。由于我的接口不穩(wěn)定,有時候會非常慢,我設(shè)置的是20s,通常5s就可以了。

renderer.headless為表示是否以無頭模式運行,無頭即不展示界面,如果設(shè)置為false,則會在瀏覽器加載頁面時候展示出來,一般用于調(diào)試。

// webpack.config.js
const path = require('path');
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer

module.exports = {
  plugins: [
    // ... other config
    new PrerenderSPAPlugin({
      // 必填,打包的文件輸出目錄,預(yù)渲染工具會在這個目錄啟動express服務(wù)
      staticDir: path.join(__dirname, 'dist'),
      // 必填,指定需要預(yù)渲染的路由
      routes: [ '/', '/about', '/some/deep/nested/route' ],
      renderer: new Renderer({
          renderAfterTime: 5000,
          headless: false
      })
      
    }),
  ]
};

其他選項請參考官網(wǎng)說明。

三、數(shù)據(jù)初始化問題。

即使預(yù)渲染了首屏頁面,但是使用React渲染界面,每次還是會重新請求,并且數(shù)據(jù)響應(yīng)之前,因為沒數(shù)據(jù),所以渲染的界面還是沒有題目列表,處于loading狀態(tài)。

// home-page.js
import {useState, useEffect} from 'react';
import * as quizService from '@/service/quiz';

const Home = () => {
  const [quizList, setQuizList] = useState(null);
  
  useEffect(() => {
    quizService.list()
      .then(res => {
      res.data && setQuizList(res.data);
    });
  }, []);
  
  return (
    <div className="home-container">
      <h1>前端題庫</h1>
      {
        quizList
            ? quizList.map(({title, id}) => (
                <div key={id}>
                    <button type="link" >{title}</button>
                </div>
            ))
            : <div>loading...</div>
      }
    </div>
  );
};

export default Home;

解決方法是,預(yù)渲染時候把數(shù)據(jù)掛到頁面標(biāo)簽上,然后用戶訪問時候,判斷頁面標(biāo)簽上有數(shù)據(jù),直接取下來用。

// home-page.js
import {useState, useEffect} from 'react';
import * as quizService from '@/service/quiz';

const Home = () => {
    const [quizList, setQuizList] = useState(null);

    useEffect(() => {
        // 用戶訪問時候,發(fā)現(xiàn)頁面上有保存數(shù)據(jù)的標(biāo)簽,則直接取出數(shù)據(jù)用,不用請求接口
        if (document.getElementById('quiz_list')) {
            setQuizList(JSON.parse(document.getElementById('quiz_list').getAttribute('data-value')));
            return;
        }
        // 預(yù)渲染時候,請求接口并存放在標(biāo)簽上
        quizService.list()
            .then(res => {
                const {data} = res;
                data && setQuizList(data);
                const script = document.createElement('script');
                script.id = 'quiz_list';
                script.setAttribute('data-value', JSON.stringify(data));
                document.head.appendChild(script);
            });
    }, []);

    return (
        <div className="home-container">
            <h1>前端題庫</h1>
            {
                quizList
                    ? quizList.map(({title, id}) => (
                        <div key={id}>
                            <button type="link" >{title}</button>
                        </div>
                    ))
                    : <div>loading...</div>
            }
        </div>
    );
};

export default Home;

如果有很多頁面需要預(yù)渲染,我們可以把預(yù)渲染的數(shù)據(jù)初始化邏輯封裝成一個自定義hook:

// usePrerenderer.js
function usePrerenderer(id, fetchData, deps) {
    const [initState, setInitState] = useState(null);
    useEffect(() => {
        // 如果標(biāo)簽上有數(shù)據(jù),則直接設(shè)置
        if (document.getElementById(id || '__INIT_STATE__')) {
            setInitState(JSON.parse(document.getElementById(id).getAttribute('data-value')));
            document.head.removeChild(id);
        }
        else {
            fetchData()
                .then(res => {
                    const {data} = res;
                    data && setInitState(data);
                    const script = document.createElement('script');
                    script.id = id;
                    script.setAttribute('data-value', JSON.stringify(data));
                    document.head.appendChild(script);
                });
        }
    }, deps);

    return [initState, setInitState];
};

使用usePrerenderer,主頁我們可以這樣寫

// home-page.js
import * as quizService from '@/service/quiz';
import usePrerenderer from './usePrerenderer';

const Home = () => {
    const [quizList, setQuizList] = usePrerenderer(
        '__QUIZ_LIST__',
        () => quizService.list(),
        []
    );

    return (
        <div className="home-container">
            <h1>前端題庫</h1>
            {
                quizList
                    ? quizList.map(({title, id}) => (
                        <div key={id}>
                            <button type="link" >{title}</button>
                        </div>
                    ))
                    : <div>loading...</div>
            }
        </div>
    );
};

export default Home;

四、結(jié)語

前端預(yù)渲染的優(yōu)點:

  • 相對于動態(tài)取數(shù)據(jù)的客戶端渲染,預(yù)渲染的SEO更好。
  • 是一個提升首屏速度的輕量方案,引入插件,配置渲染路徑和打包路徑,完成!

預(yù)渲染的問題:

  • 場景受限,數(shù)據(jù)不經(jīng)常變動,且是死數(shù)據(jù),隨時間變化的動態(tài)數(shù)據(jù)就不能用這種方案。
  • 每次修改首屏數(shù)據(jù),都需要重新構(gòu)建。

預(yù)渲染和靜態(tài)站點生成器有一些共同之處,它們都是在發(fā)布之前渲染為靜態(tài)頁面。

預(yù)渲染和靜態(tài)站點生成器有一定區(qū)別

  • 靜態(tài)站點生成器開發(fā)項目,項目目標(biāo)就是生成一個靜態(tài)站點,而預(yù)渲染是給普通網(wǎng)站提速的手段。
  • 預(yù)渲染可以用也可以不用,不用的話也就是速度慢一點,不用靜態(tài)站點生成器生成話,項目沒法發(fā)布
  • 原理不同,靜態(tài)站點生成器模板渲染,預(yù)渲染是使用的無頭瀏覽器截圖
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容