Next.js頁面渲染的優化方案

雲幀數浪發表於2019-01-26

在過去一年的工作中我所使用的js框架是Next.js,儘管這個框架在前後端同構方面有著絕佳的體驗,但是當頁面js檔案過大以及preload過多的時候還是會出現頁面跳轉卡頓和渲染阻塞等比較糟糕的使用者體驗問題。由於我之前既不知道這個框架的工作原理,自然也就不知道如何去優化它。乘著農曆春節前工地活少所以稍微研究一下。

第一個問題:宣稱前後臺同構的Next.js為何會出現卡頓現象?

Next.js 中的特有生命週期hook 函式 getInitialProps會在頁面渲染的時候判斷瀏覽器是否為首次渲染,如果是則是服務端渲染網頁,如果不是則是客戶端渲染。在頁面首次渲染的時候,會載入commons.xxxxx.js檔案,這個檔案中打包了react.js、next.js 以及相關的框架程式碼也就是如果是客戶端渲染打包後的commons.xxxxx.js負載了整個前端的頁面邏輯,這個檔案相對比較大一般會在180kb以上。如果僅從檔案大小角度來說,這個檔案並不算大,就算利用了next.js 的 preload機制把檔案大小放到300kb以上,也還行。但是一旦這個檔案阻塞了頁面的渲染,頁面的渲染要等到commons.xxxxx.js載入完畢之後才渲染,那問題就來了。

在next7中使用的打包工具是webpack4,這在打包和載入過程有一個比較蠢的機制(或許僅僅是我個人觀點),那就是但凡React DOM上繫結了style 這些DOM都不會在服務端渲染出來,而是打包抽離成一個小的js檔案,在commons.xxxxx.js載入完畢之後,再載入這個js,將DOM和內聯style渲染到HTML。這就在某種程度上導致了next.js首次渲染是SSR失效了,更為糟糕的是卡頓感十足。

可能有人會說,那就不要寫內聯style不就好了。但是事實是在大量的後臺資料動態渲染頁面和使用者自定義頁面的情況下,不可能做到完全不寫內聯樣式,而去傻乎乎地寫一堆className。

所以我們要解決一個問題那就是如何保證,內聯style的react dom在首次渲染頁面的時候是伺服器端直接輸出後扔給後臺,而不是讓commons.xxxxx.js卡卡卡卡卡,然後砰的一下蹦出來。

要解決上一個問題,首先要了解Next.js是如何渲染頁面的?

在Next.js的規則中,所有頁面級的程式碼都是寫在pages資料夾中,比如/pages/home:

export default () => (<div>你瞅啥?這是home頁</div>)
複製程式碼

而其框架內建的Document元件中,已經幫開發者配置好傳統的HTML檔案的<head>,<body>這些標籤作為靜態資源的外殼。Document元件中有一個renderPage()方法,如果程式碼正常執行,該方法就會將pages資料夾中的程式碼和它外部同步渲染到瀏覽器中。如果開發者希望自定義Document元件只需新增/pages/_document.js檔案即可。

renderPage()本質是一個回撥函式,它的作用只有一個那就是執行React原始碼中渲染邏輯同步載入到Next.js的Document元件中形成DOM節點。

import Document, { Head, Main, NextScript } from 'next/document'

export default class MyDocument extends Document {
  static getInitialProps ({ renderPage }) {
    // renderPage()位於next.js特有生命週期函式getInitialProps中。 
    return renderPage();
  }

  render () {    
    return (
      <html>
        <Head>
          <title>沒見過標題黨嗎?</title>
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}
複製程式碼

服務端渲染樣式

為了能讓伺服器端渲染樣式,我們首先得先做兩件事:

  1. 在頁面首次載入的時候,也就是所謂的SSR.能讓renderPage方法在伺服器端就能對React Dom進行解析,讓HTML歸HTML,CSS歸CSS;

  2. 能讓Document元件在頁面切換時,能及時更新<head>,這樣不同的頁面就能載入自己所需的script,style。


解決方案的登場

隆重介紹神器styled-components出場,styled-components在github上目前為止已經超過1萬stars,它的設計初衷在於在服務端渲染的時候,同時渲染出一個ServerStyleSheet,然後把這個ServerStyleSheet送入React DOM樹中。它主要就做兩件事:

  1. 把元件中styles抽離到<style>標籤中;
  2. <style>標籤放到<head>

下面就是一段如何正確使用ServerStyleSheet的姿勢步驟:

  import { ServerStyleSheet } from "styled-components";

  static getInitialProps ({ renderPage }) {
    const sheet = new ServerStyleSheet()
    const transform = (App) => {
      return sheet.collectStyles(<App />);      
    }
    const styleTags = sheet.getStyleElement()
    const page = renderPage(transform);
    return { ...page, styleTags };
  }
  
  render(){
      return(
       <html lang="zh-Hans">
        <Head>
          <meta name="viewport" content="initial-scale=1.0, width=device-width" />
          <meta name="description" content="Kanseefoil"/>
          <link rel="shortcut icon" href="/static/favicon.ico"></link>
          {this.props.styleTags}
        </Head>
        </html>
       );
  }
複製程式碼

上面的程式碼已經完美跟大家展示瞭如何將內聯style抽離出dom,然後通過<link style>的方法渲染樣式, 那麼問題來了,如何在打包解析react dom時,給伺服器一個"純潔、乾淨、無暇"的DOM呢?

這個時候就需要使用babel-plugin-styled-components包,在babel中進行解析。

程式碼如下:

{
    "presets": [
        "next/babel"
    ],
    "plugins": [
        ["styled-components", { "ssr": true, "displayName": true, "preprocess": false } ]
    ]
}
複製程式碼

這個時候在去開啟next.js頁面就會發現,那傢伙、那場面渲染速度嗖嗖的。至於負責前端邏輯的commons.xxxxx.js,您老人家就安靜地慢慢地載入吧。

相關文章