用 Next.js 與 TailwindCSS 創建效能良好的網站

picture

2023-10-02

用 Next.js 與 TailwindCSS 創建效能良好的網站

1. 效能指標介紹

可參考: web-vitals

  • 測試工具 : 使用 Google 的 PageSpeed Insights 來檢測頁面加載速度。
  • First Contentful Paint (FCP) : 衡量當第一塊內容(例如文字或圖像)在用戶的屏幕上被渲染的時間。 建議 完成時間為 2.5 秒內 。
  • Largest Contentful Paint (LCP) : 衡量最大內容元素(例如圖片或文本區塊)完成渲染在屏幕上的時間。 建 議頁面之 FID 應低於 100 毫秒 。
  • Total Blocking Time (TBT) : 衡量在首次繪製和完全交互之間所有阻塞事件的總時間。 建議完成時間為 200 毫秒內 。
  • Cumulative Layout Shift (CLS) : 衡量視覺上的內容如何在頁面加載時移動,代表布局的穩定性。 建議分數 應低於 0.1 。
  • Speed Index (SI) : 衡量頁面內容呈現速度的指標,反映出用戶看到頁面內容的速度。 建議完成時間在 3.5 秒以內。

2. 改善 First Contentful Paint (FCP)

利用 Next.js 的服務器渲染(SSR)或靜態網站生成(SSG)特性,能更快地提供首次內容,從而改善 FCP。

可參考: 什麼時候用 Server Side Rendering 與 Static Side Generation

以下簡單解釋 SSG 跟 SSR 的差別跟例子:

SSG

  • 想像你要開一個咖啡店,但是你只賣一種咖啡。每個客人來了都要等你現場煮,這樣會很慢對吧?如果你事先煮 好一大壺咖啡,每個客人來了就直接倒給他們,這樣多快多好!SSG 就是這樣,它事先把所有的網頁都“煮”好( 也就是生成好),等到有人來訪問的時候,直接給他們。
  • 頁面在構建時生成,並且每個請求都重用相同的 HTML。這提供了極佳的性能和 SEO 優勢。在 Next.js 裡,你 只要在pages 目錄下建一個檔案(比如 about.js),然後 Next.js 在 build 階段會自動把它變成一個靜 態頁面。
// pages/data.js
export default function Data({data}) {
  // 假設資料是一個包含多個項目的陣列
  return (
    <div>
      <h1>資料列表:</h1>
      <ul>
        {data.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export async function getStaticProps() {
  // 從API獲取數據
  const res = await fetch('<https://api.example.com/data>');
  const data = await res.json();

  // 返回數據作為props
  return {
    props: {data},
    revalidate: 1, // 每秒重新生成頁面一次
  };
}

SSR

  • 現在想像你的咖啡店開始賣很多種不同的咖啡。每個客人來了都可以點不同的咖啡,這時候你不能事先都煮好, 因為你不知道他們要什麼。但是你可以等他們來了之後,現場為他們煮他們想要的咖啡。這樣每個人都可以喝到 他們想要的咖啡,但是他們可能要等一下。SSR 就是這樣,每次有人來訪問網站的時候,它都會為他們生成一個 新鮮的頁面。
  • 每次請求時,都會在伺服器上實時生成頁面。這有利於 SEO 並且允許頁面內容根據用戶請求動態更改。在這個 例子裡,每次有人訪問/profile 頁面的時候, getServerSideProps會在伺服器上運行,並且生成一個新的 頁面給用戶。
// pages/profile.js
export default function Profile({username}) {
  return <div>Hello, {username}!</div>;
}

export async function getServerSideProps(context) {
  // 假設我們從一個API獲取用戶名
  const response = await fetch('<https://api.example.com/user>');
  const data = await response.json();

  return {
    props: {
      username: data.username,
    },
  };
}

3. 改善 Largest Contentful Paint (LCP)

優化圖像和媒體檔案大小,並利用 Next.js 的Image組件來實現圖片的延遲加載和自動格式轉換。

import Image from 'next/image';

function MyImageComponent() {
  return <Image src="/my-image.jpg" alt="Picture" width={500} height={500} />;
}

4. 改善 Total Blocking Time (TBT)

避免大量的 JavaScript 在主線程上執行,利用 Next.js 的程式碼拆分和懶加載特性來減少主線程的阻塞時間。

import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(() => import('../components/hello'));

function MyPage() {
  return <DynamicComponent />;
}

5. 改善 Cumulative Layout Shift (CLS)

保持布局的穩定性,避免在加載時元素的移動。指定圖片和影片的尺寸,並避免在加載時插入新內容。

<img src="/my-image.jpg" alt="Picture" width="500" height="500" />

6. 改善 Speed Index (SI)

利用 Next.js 的優化特性,例如程式碼拆分、懶加載、和 SSR 或 SSG 來改善頁面的加載速度和交互速度。此外 ,配置 CDN 和優化靜態資源的傳送也能有助於提高 Speed Index。

配置 Content Delivery Network (CDN)

Content Delivery Network (CDN) 是一種服務,它通過在地理上分散的伺服器來儲存和提供網站的靜態資源(例 如圖片、CSS、JavaScript)。當用戶訪問網站時,CDN 會從離用戶最近的節點提供資源,從而減少了加載時間。 配置 CDN 的基本步驟如下:

  1. 選擇 CDN 提供商 :
  • 選擇一個 CDN 提供商,例如 Cloudflare、AWS CloudFront 或 Google Cloud CDN。
  1. 註冊和設置 CDN 賬戶 :
  • 跟隨 CDN 提供商的指南註冊並設置你的賬戶。
  1. 配置 CDN :
  • 在 CDN 控制台中,創建一個新的 CDN 分發並配置你的網站的域名和源伺服器地址。
  1. 更新 DNS 記錄 :
  • 更新你的域名的 DNS 記錄,以指向 CDN 服務而不是你的原始伺服器。
  1. 驗證配置 :
  • 確保你的網站通過 CDN 正確地傳遞,並檢查加載速度是否有所改善。

避免不必要的重新渲染

檢查重新渲染的工具

勾選 Paint flashing 之後,會在 component 被重新繪製時標註螢光色

image

React Developer Tools

  • 從 Chrome 下載 React Developer Tools 之後,打開瀏覽器 dev tool 可以看到 ComponentsProfiler。其中 Profiler 可以錄製在網站重整 之後用戶在網站執行操作之後造成的 render。

  • 勾選 Highlight updates when components render ,在組件重新渲染時,可以看到組件被線條框住,綠色線 條代表重新渲染的次數較少,黃色線條代表被重新渲染許多次。

  • 在造成渲染的原因中,“This is the first time the component rendered.”是正常的原因,其他原因如 “The parent component rendered.”跟“Context changed” 等等是效能優化時須斟酌是否能改善的地方

image

image

用 React.memo

React.memo 是一個 React 提供的高階組件,它可以讓你在組件的 props 未改變時避免重新渲染。當你將一個 組件包裹在 React.memo 中,該組件只有在其 props 發生變化時才會重新渲染。

const MemoizedComponent = memo(SomeComponent);

這意味著,即使父組件重新渲染,只要 MemoizedComponent 的 props 沒有改變,它就不會重新渲染。這可以提 高 React 應用的性能,特別是在大型應用中。

用 useMemo 跟 useCallback

  • 當使用 React.memo 時,你的組件會在任何 prop 不與之前的值淺比較相等(shallow equality)時重新渲染。 這意味著 React 使用 Object.is 比較來確定 props 是否有所改變。例如, Object.is(3, 3) 返回 true ,但 Object.is({}, {}) 返回 false
  • 要充分利用 React.memo ,你應該盡量減少 props 的改變次數。例如,如果 prop 是一個 object,你可以使 用 useMemo 防止父組件每次重新渲染時都重新創建該 object:
function Page() {
  const [name, setName] = useState('Taylor');
  const [age, setAge] = useState(42);
  const person = useMemo(() => ({name, age}), [name, age]);
  return <Profile person={person} />;
}

避免在 component 裡創造新的 static object 或 static array

在 component 裡面的 object 或 array,會隨著 re-render 而重新計算或重新生成,如果是靜態變數,可以搬到 component 外面,避免重新生成。

const options = {
  serverUrl: 'https://localhost:1234',
  roomId: 'music'
};

function ChatRoom() {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ All dependencies declared
  // ...

使用狀態管理工具

  • 打開 React developer tool 的 Highlight updates when components render,從 Code Sandbox example 可以看到 context 會造成所有組件重新渲染,但使用狀態管理工具則可以避免不必要的組件重新渲染
  • context 的寫法是用 provider 包住所有組件,這樣會在 provider 值更新後,重新渲染 WinnerPlayers
    export default function ContextComponent() {
      return (
        <div className={styles.container}>
          <main className={styles.main}>
            <StoreContextProvider>
              <Winner />
              <Players />
            </StoreContextProvider>
          </main>
        </div>
      );
    }
    
  • 使用 Zustand 則可以只讓該 PlayerWinner 重新渲染

TailwindCSS 的 CSS 優化解法

可通過以下配置跟指令壓縮 CSS,但在壓縮前後,透過 dev tool 的 Network 看到 CSS 大小跟時間沒有太大差別 。

  • 壓縮前 image
  • 壓縮後 image
  • npx tailwindcss -o build.css --minify
// postcss.config.js

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    ...(process.env.NODE_ENV === 'production' ? {cssnano: {}} : {}),
  },
};

參考資料

shirley_avatar

Shirley Chang

软件工程师

「最近想买个萤幕,来去无印良品逛逛好了」是这样思路的一个人。

查看作者的其他文章

分享到

回上页