一文了解 NextJS 並對性能優化做出最佳實踐

引言

從本文中,我將從是什麼爲什麼怎麼做來爲大家闡述 NextJS 以及如何優化 NextJS 應用體驗。

一、NextJS 是什麼

NextJS是一款基於 React 進行 web 應用開發的框架,它以極快的應用體驗而聞名,內置 Sass、Less、ES 等特性,開箱即用。SSR 只是 NextJS 的一種場景而已,它擁有4種渲染模式,我們需要爲自己的應用選擇正確的渲染模式:

  • Client Side Rendering (CSR)
    客戶端渲染,往往是一個 SPA(單頁面應用),HTML 文件僅包含 JS\CSS 資源,不涉及頁面內容,頁面內容需要瀏覽器解析 JS 後二次渲染。
  • Static Site Generation (SSG)
    靜態頁面生成,對於不需要頻繁更新的靜態頁面內容,適合 SSR,不依賴服務端。
  • Server Side Rendering (SSR)
    服務端渲染,對於需要頻繁更新的靜態頁面內容,更適合使用 SSR,依賴服務端。
  • IncreIncremental Site Rendering (ISR)
    增量靜態生成,基於頁面內容的緩存機制,僅對未緩存過的靜態頁面進行生成,依賴服務端。

SSG / ISR 都是非常適合博客類應用的,區別在於SSG是構建時生成,效率較低,ISR是基於已有的緩存按需生成,效率更高

image.png

二、爲什麼選 NextJS

優點:

  1. 首屏加載速度快
    我們的內嵌場景比較豐富,因此比較追求頁面的一個首屏體驗,NextJS 的產物類似 MPA(多頁面應用),在請求頁面時會對當前頁面的資源進行按需加載,而不是去加載整個應用, 相對於 SPA 而言,可以實現更爲極致的用戶體驗。
  2. SEO 優化好
    SSR \ SSG \ ISR 支持頁面內容預加載,提升了搜索引擎的友好性。
  3. 內置特性易用且極致
    NextJS 內置 getStaticPropsgetServerSidePropsnext/imagenext/linknext/script等特性,充分利用該框架的這些特性,爲你的用戶提供更高層次的體驗,這些內容後文會細講。

缺點:

  1. 頁面響應相對於 SPA 而言更慢
    由於頁面資源分頁面按需加載,每次路由發生變化都需要加載新的資源,優化不夠好的話,會導致頁面卡頓
  2. 開發體驗不夠友好
    開發環境下 NextJS 根據當前頁面按需進行資源實時構建,影響開發及調試體驗

三、如何將 NextJS 應用體驗提升到極致

作爲一名開發者,我們追求的不應該是應用能用就好,而是好用,那麼如何評價我們的應用是否好用呢?

  • 最直接的方案當然是通過收集用戶反饋來評判
  • 從開發層面,最直觀的就是通過performancelighthouse來評判

3.1 優化前

如你所見,由於應用模塊的一個複雜性,我們的 NextJS 應用起初性能並不是很好,甚至談得上是差

  • FCP: 首次內容繪製時間 1.8s
    image.png

  • lighthouse: 性能評分報告 55 分,Time to Interactive(TTI) 可交互時間爲 7.3s,通常是發生在頁面依賴的資源已經加載完成。

  • network: 我們每次進行路由跳轉都要按需加載資源,因此我們需要單個頁面的 DomContentLoaded 儘可能快以保證頁面 Dom 結構的渲染效率。

    image.png

  • 頁面構建時間

    ![image.png](https://img12.360buyimg.com/img/s754x284_jfs/t1/101480/17/34324/94579/635a1b57E31984e57/bfc7eb30cd6bd34d.png)
    

    這些指標都間接反饋出應用的體驗問題亟待解決。
    image.png

3.2 優化措施

  • 優化用戶體驗
    • 1. 開啓 gzip 壓縮
      通過 network 可以看到資源實際大小及 http 請求的 size,如果不開啓壓縮,二者基本是沒有差異的。
      image.png
      gzip 優化後可以看到, 壓縮效果還是很明顯的
      開啓 nginx 的 gzip 壓縮

      ```shell
      gzip                            on;
      gzip_min_length                 100;
      gzip_buffers                    4 16k;
      # gzip_http_version               1.0;
      gzip_comp_level                 9;
      gzip_types                      gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png application/javascript;
      gzip_vary                       on;
      gzip_proxied                       any;
      ```
      
      通過 response header 判斷壓縮是否生效
      ![image.png](https://img12.360buyimg.com/img/s356x278_jfs/t1/123411/6/32476/40551/635a1e58E1bef9e2f/8e931332a588fc1e.png)
      
    • 2. 針對非首屏組件基於 dynamic 動態加載
      在頁面加載過程中,針對一些不可見組件,我們應該動態導入,而不是正常導入,確保只有需要該組件的場景下,才 fetch 對應資源, 通過 next/dynamic,在構建時,框架層面會幫我們進行分包

      import dynamic from 'next/dynamic'
      const Modal = dynamic(() => import('../components/mModal'));
      export default function Index() {
        return (
          {showModal && <Modal />}
        )
      }
      

      打開 Network。當條件滿足時,你將看到一個新的網絡請求被髮出來獲取動態組件(單擊按鈕打開一個模態)。

    • 3 . next/script 優化 script 加載時
      next/script 可以幫助我們來決定 js 腳本加載的時機

      strategy | 描述
      ---- | ---
      beforeInteractive | 可交互前加載腳本
      afterInteractive | 可交互後加載腳本
      lazyOnload | 瀏覽器空閒時加載腳本
      
      ```html
      <Script strategy="lazyOnload" src="//wl.jd.com/boomerang.min.js" />
      ```
      
    • 4. next/image 優化圖片資源
      next/image 可幫助我們對圖片進行壓縮(尺寸 or 質量),且支持圖片懶加載,默認 loader 依賴 nextjs 內置服務,也可以通過{loader: custom}自定義 loader

      import Image from "next/image";
      const myLoader = ({ src, width, quality }) => {
        return `https://example.com/${src}?w=${width}&q=${quality || 75}`;
      };
      const MyImage = (props) => {
        return (
          <Image
            loader={myLoader}
            src="me.png"
            alt="Picture of the author"
            width={500}
            height={500}
          />
        );
      };
      
    • 5. next/link 預加載
      基於 hover 識別用戶意圖,當用戶 hover 到 Link 標籤時,對即將跳轉的頁面資源進行預加載,進一步防止頁面卡頓

      import Link from "next/link";
      <Link prefetch={false} href={href}>
        目標頁面
      </Link>;
      
    • 6. 靜態內容預加載
      基於 getStaticProps 對不需要權限的內容進行預加載,它將在 NextJS 構建時被編譯到頁面中,減少了 http 請求數量

      function Blog({ posts }) {
        return (
          <ul>
            {posts.map((post) => (
              <li>{post.title}</li>
            ))}
          </ul>
        );
      }
      export async function getStaticProps() {
        const res = await fetch("https://.../posts");
        const posts = await res.json();
        return {
          props: {
            posts,
          },
        };
      }
      export default Blog;
      
    • 7. 第三方 library 過大時,基於 umd 按需加載
      第三方 library 過大時,以 umd 進行引入,在需要的場景下通過 script 進行加載。

      // 封裝掛載 umd 模塊的 hoc
      function loadUmdHoc(Comp: (props) => JSX.Element, src: string) {
      return function Hoc(props) {
      const [isLoaded, setLoaded] = useState(
      !!Array.from(document.body.getElementsByTagName('script')).filter(
      (item) => item.src.match(src)
      ).length
      )
      useEffect(() => {
      if (isLoaded) return
      const script = document.createElement('script')
      script.src = src
      script.onload = () => {
      setLoaded(true)
      }
      document.body.append(script)
      }, [])
      if (isLoaded) {
      return <Comp {...props} />
      }
      return <></>
      }
      }
          function Upload(){
            // todo 使用umd模塊
            return <></>
          }
      
          // 使用該組件時,加載hoc
          export default loadUmdHoc(
            Upload,
            'xxx.umd.js'
          )
          ```
      
  • 優化用戶體驗
    • 1. 基於 urlimport 進行瘦身,提升編譯效率
      urlImport 是 NextJS 提供的一個實驗特性,支持加載遠程 esmodule
      image.png
      NextJS 會在本地對所加載的遠程模塊進行緩存, 減少了我們所需構建的模塊數,缺點是它會影響 treeShaking 的一個效果,因此在生產環境,建議通過NormalModuleReplacementPlugin對 urlimport 的依賴進行一個本地替換
      image.png
      • 2. webpack 配置選擇性忽略
        針對一些生成環境的配置我們可以通過區分環境來進行選擇性忽略部分配置,如 module federation exposes 在開發環境我們就可以忽略掉。

        dev.conf.js
        image.png
        pro.conf.js
        image.png

      • 3. 開啓 SWC 編譯
        SWC 是基於 Rust 實現的一款開發工具,既可用於編譯也可用於打包,據官方言,它比 Babel 快了 20~70倍,NextJS 在 12 版本默認打開了 SWC 的支持。開啓 SWC 後,應用的編譯速度將比 Babel 快 17 倍,刷新速度快 5 倍。需要注意的是如果你通過.babelrc自定義 babel 配置,SWC 的一些特性將會被關閉。

3.3 優化後

從以下指標可以看出我們應用的體驗得到了很大提升, 實際的一個交互體驗也好了不少,在路由跳轉上實現了類似 SPA 的一個體驗,即使是各頁面資源按需加載不會再出現頁面卡頓的情況。
image.png

  • FCP: 首次內容繪製時間 從 1.8s 優化到 0.35s,提升了近 80%
    image.png
  • lighthouse: 評分從 55 提升到了 80,TTI 從 7.3s 優化到了 2.4s, 分別提升了 30% / 64%,chrome 的最佳實踐分達到了滿分💯
  • network: DomContentLoaded 從 2.42s 優化到 0.67s,提升了 77%
    image.png
  • 頁面構建時間: 基本滿足了毫秒級實現頁面編譯的需求,提升了 70% 以上
    image.png

image.png

四、後續規劃

爲了實現更爲極致的用戶體驗,我們後續計劃將資源上 CDN,減少Waiting for server response的性能損耗,並加入PWA的離線緩存特性。
參考文章
Optimize Next.js App Bundle and Improve Its Performance
我看 Next.js:一個更現代的海王

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章