跳至主要内容

9 篇文章 含有標籤「ES Module」

檢視所有標籤

React 效能優化入門教學筆記 | 學習筆記

· 閱讀時間約 6 分鐘
kdchang

前言

隨著前端應用日益龐大,單頁應用(SPA)在初次載入時常面臨 JavaScript 檔案過大、載入時間過久的問題,導致使用者等待時間過長、效能下降。為了解決這個問題,React 與現代建構工具(如 Webpack、Vite)提供了 Code Splitting(程式碼分割)與 Lazy Loading(延遲載入)兩種策略,協助開發者更有效地管理與優化應用程式的載入流程。

程式碼分割(Code Splitting)和惰性載入(Lazy Loading)都是用來優化網頁效能的方法,它們都旨在減少初始加載時間,但實現方式和目標略有不同。 程式碼分割是將程式碼分割成多個較小的塊,而惰性載入則是在需要時才加載這些塊。

一、程式碼分割(Code Splitting)

概念:

程式碼分割是將一個大型的 JavaScript 應用程式分割成多個較小的、獨立的塊,每個塊包含應用程式的一部分程式碼。 這些塊通常是根據路由、元件或功能來分割的。

目標:

主要目標是減少應用程式的初始加載時間,通過只加載使用者當前需要的程式碼塊,而不是一次性加載所有程式碼。

實現方式:

程式碼分割通常使用打包工具(如 Webpack、Rollup 等)和動態import()語法來實現。

使用時機:

在編譯時(build time)就進行分割。

二、惰性載入(Lazy Loading)

概念:

惰性載入是指在需要的時候才加載程式碼,而不是在應用程式初始化時就加載所有程式碼。

目標:

減少應用程式的初始加載時間,特別是對於大型應用程式或元件。

實現方式:

惰性載入通常使用 React.lazy 和 Suspense 元件來實現,也可以配合程式碼分割一起使用。

使用時機:

在執行時(runtime)才加載,通常是當使用者訪問某個路由、觸發某個事件或需要顯示某個元件時。

差異總結

特性程式碼分割 Code Splitting惰性載入 Lazy Loading
概念將程式碼分割成多個塊在需要時才加載程式碼
目標減少初始加載時間,優化效能減少初始加載時間,優化效能
實現方式打包工具,dynamic import()React.lazy, Suspense, dynamic import()
時機編譯時執行時
關聯性程式碼分割是惰性載入的基礎,惰性載入可以利用程式碼分割的結果。

重點摘要

  • Code Splitting(程式碼分割)

    • 是一種將整個應用程式切割成多個檔案的技術
    • 通常由 Webpack、Rollup 等建構工具自動處理
    • 可應用於 route-based 分割、component-based 分割等情境
    • 不代表一定是延遲載入,僅是結構上的切割
  • Lazy Loading(延遲載入)

    • 是一種執行時載入程式碼的策略
    • 常與 import() 搭配,直到實際使用時才載入
    • 通常透過 React.lazySuspense 實現元件的懶載入
    • 是 Code Splitting 的使用方式之一
  • 兩者關係

    • Code Splitting 是靜態建構階段的優化策略
    • Lazy Loading 是執行階段的載入行為
    • Lazy Loading 必須建立在已做 Code Splitting 的前提上
  • 效益

    • 減少主程式 bundle 的大小
    • 提升首次載入速度(First Contentful Paint)
    • 延遲不必要的資源載入,節省頻寬與記憶體

實際範例

範例一:傳統未分割的情況(單一 bundle)

// App.js
import HomePage from './HomePage';
import Dashboard from './Dashboard';

function App() {
return (
<>
<HomePage />
<Dashboard />
</>
);
}

這樣寫會導致 HomePage 和 Dashboard 在應用一開始就被載入,無論使用者有沒有看到這些元件。


範例二:使用 React.lazy 實現 Lazy Loading 與 Code Splitting

// App.js
import React, { Suspense } from 'react';

// Lazy Loading:只有在渲染時才動態 import
const HomePage = React.lazy(() => import('./HomePage'));
const Dashboard = React.lazy(() => import('./Dashboard'));

function App() {
return (
<Suspense fallback={<div>載入中...</div>}>
<HomePage />
<Dashboard />
</Suspense>
);
}

使用 React.lazy() 搭配 import() 會讓 Webpack 將這些元件建立為獨立的 chunk。 真正渲染時(如使用者切換頁面),才會觸發載入行為,減少初始 bundle 體積。


範例三:Route-based Code Splitting(React Router)

// AppRouter.js
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

export default function AppRouter() {
return (
<Router>
<Suspense fallback={<div>頁面載入中...</div>}>
<Switch>
<Route path="/home" component={Home} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</Suspense>
</Router>
);
}

使用路由為單位切割頁面元件,是最常見的 Code Splitting 實務做法。


範例四:動態 import 實作非元件的延遲載入

// utils.js
export function heavyCalculation(input) {
// 假設這段計算非常耗時
return input ** 10;
}
// App.js
function handleClick() {
import('./utils').then(({ heavyCalculation }) => {
const result = heavyCalculation(5);
console.log(result);
});
}

在某些不需要立即執行的邏輯或大型工具函式庫,也可以透過 import() 動態載入來延遲其成本。


常見問題與補充

Q1:Code Splitting 是自動的嗎?

  • 大部分情況下需要手動設計入口點(如 React.lazyimport()),Webpack 才會建立分離的 chunk。

Q2:只有使用 React.lazy 才能 Lazy Load 嗎?

  • 不一定,import() 是底層機制,也可配合其他框架(Vue、Svelte)或工具(React Loadable)使用。

Q3:懶載入的元件可以預載嗎?

  • 可以,透過 import().then() 觸發一次即可放進瀏覽器快取,達到「預熱」效果。

總結

在前端應用越來越大型化的今天,掌握 Code Splitting 與 Lazy Loading 的差異與使用場景,已成為每位前端工程師的必備技能。Code Splitting 解決的是「結構上的模組分離」,Lazy Loading 則是「載入時機的延後」。兩者密不可分,但用法與思考層次不同。

實務上可先針對頁面級路由進行分割,再進一步優化元件級的載入、工具模組載入時機,逐步降低初始 bundle 體積,提升網站效能與使用者體驗。

參考文件

  1. React | 為太龐大的程式碼做 Lazy Loading 和 Code Splitting

React 效能優化入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

前言

React 作為現代前端開發的主流函式庫之一,強調 UI 的組件化與狀態驅動式渲染。然而,隨著應用規模擴大與資料變得動態頻繁,React 應用可能出現重新渲染過多、載入過慢或記憶體占用過高等問題,影響使用者體驗與開發效率。為此,瞭解與掌握 React 的效能優化技巧,成為中高階開發者的重要功課。

本篇筆記將介紹 React 效能優化的核心原則與常見實作方式,搭配簡單的程式碼範例說明實際操作,協助你建立清晰的優化思維與實作經驗。


重點摘要

  • 避免不必要的重新渲染

    • 使用 React.memo 包裹純函式組件
    • 適當使用 useMemouseCallback 記憶運算結果或函式引用
  • Lazy loading(Code Splitting)

    • 使用 React.lazySuspense 實現組件按需載入
  • 列表渲染優化

    • 提供穩定的 key,避免 diff 錯誤導致重繪
    • 處理大量資料時可結合虛擬化工具(如 react-window
  • 狀態管理與邏輯分離

    • 將全域狀態與 UI 狀態分離,減少重渲染範圍
    • 減少 props 傳遞鏈,避免深層組件無謂更新
  • 避免 inline 宣告與函式

    • 每次 render 都會產生新函式或物件,導致子組件重新渲染
  • 效能分析與工具

    • 使用 React DevTools 的 Profiler 模組分析 render 開銷
    • 善用 Chrome DevTools、Lighthouse 等協助調校效能

實際範例

1. 避免不必要的渲染:使用 React.memo

// 子元件
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
console.log('Render:', todo.text);
return (
<li>
<input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} />
{todo.text}
</li>
);
});

若未使用 React.memo,即使 todo 資料未變,只要父層重新 render,TodoItem 就會跟著重新 render。使用 React.memo 可避免這種不必要的重新渲染。


2. 函式記憶:使用 useCallback

const onToggle = useCallback((id) => {
setTodos((prev) =>
prev.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo))
);
}, []);

如果 onToggle 每次 render 都重新宣告,會導致 React.memo 判斷 props 改變,從而重新渲染子元件。使用 useCallback 可以保留函式參考的一致性。


3. 虛擬滾動列表:使用 react-window

import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => <div style={style}>Row {index}</div>;

const MyList = () => (
<List height={300} itemCount={1000} itemSize={35} width={300}>
{Row}
</List>
);

react-window 提供虛擬滾動的能力,只 render 可視範圍內的項目,大幅減少 DOM 結點,提高大數據列表效能。


4. 懶載入元件:使用 React.lazy

import React, { Suspense } from 'react';

const Chart = React.lazy(() => import('./Chart'));

function Dashboard() {
return (
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
);
}

將大型組件分割成懶載入模組,可避免初次載入體積過大,提升頁面初始加載速度。


5. 使用 Profiler 分析效能瓶頸

React DevTools 提供 Profiler 模組,可追蹤各元件 render 時間與次數,有助於識別過度渲染或效能低落的元件。

import { Profiler } from 'react';

<Profiler
id="TodoList"
onRender={(id, phase, actualDuration) => {
console.log(`${id} rendered in ${actualDuration}ms`);
}}
>
<TodoList todos={todos} />
</Profiler>;

結語

React 效能優化並非一蹴可幾,而是需隨著應用規模與需求不斷調整與改善的過程。透過理解 Virtual DOM 的運作原理、掌握各種 Hook 的特性,以及活用分析工具,我們可以更有策略地針對效能瓶頸逐步優化,打造流暢且可維護的使用者體驗。

建議從小型優化(如 React.memouseCallback)著手,並逐步引入懶載入與虛擬化等進階技巧,讓 React 應用能夠隨著功能擴展持續保持高效能。

如果你對特定效能問題有興趣,例如圖片載入優化、CSR vs SSR 效能比較等,也可以再深入探討不同的進階主題。

React 效能優化 SOP 檢核清單入門教學筆記 | 學習筆記

· 閱讀時間約 6 分鐘
kdchang

前言

在大型單頁應用(SPA)與複雜互動式介面中,效能瓶頸常常來自不必要的重新渲染、大型 bundle 導致的載入緩慢,以及過度操作 DOM 所造成的卡頓。建立一份標準化的「效能優化檢核清單」(SOP,Standard Operating Procedure),能夠在開發流程中明確指出應檢查的重點、落實最佳實踐,並透過持續監控與回饋,進一步強化團隊的效能意識與程式品質。

本篇筆記將依照從「程式撰寫到部署」的不同階段,提出具體的檢核項目,並搭配最常見的 React 效能優化技術範例,協助你快速掌握如何在日常開發與 Code Review 中落實效能優化。


重點摘要

  • 一、避免不必要的重新渲染

    • 使用 React.memo 包裹純函式元件
    • 針對函式與物件 props,使用 useCallbackuseMemo 進行記憶
    • 避免 JSX inline 宣告函式或物件
  • 二、State 管理與元件分離

    • 下放 state 至影響範圍最小的元件
    • UI 狀態(開關、Modal 等)與業務資料分離
    • 避免全域 context 過度包覆,導致大範圍 re-render
  • 三、列表與大量資料渲染優化

    • 確保 key 穩定(使用唯一 id,非 index)
    • 採用虛擬滾動(react-windowreact-virtualized
    • 分頁或懶加載機制
  • 四、Code Splitting 與懶載入

    • 使用 React.lazy + Suspense 分割大型元件
    • 路由層級拆分,動態 import()
    • 圖片與第三方資源延遲加載
  • 五、效能分析與監控工具

    • React DevTools Profiler:分析元件 render 次數與耗時
    • Lighthouse / Web Vitals:追蹤 FCP、LCP、TTFB 等指標
    • Bundle 分析(Webpack Bundle Analyzer、Source Map Explorer)
  • 六、CI/Code Review 效能檢查

    • 將檢核清單納入 Pull Request 模板
    • 自動化檢測 bundle size 變化
    • 定期性能測試腳本(Cypress、Playwright + Lighthouse)

實際範例

範例一:避免不必要的重新渲染

// ChildComponent.jsx
import React from 'react';

function ChildComponent({ data, onClick }) {
console.log('ChildComponent render');
return <div onClick={onClick}>{data.text}</div>;
}

export default React.memo(ChildComponent);
// ParentComponent.jsx
import React, { useState, useCallback, useMemo } from 'react';
import ChildComponent from './ChildComponent';

export default function ParentComponent({ initialData }) {
const [count, setCount] = useState(0);

// useMemo 記憶 data 物件,避免因父組件重新 render 而改變 reference
const data = useMemo(
() => ({
text: initialData,
}),
[initialData]
);

// useCallback 記憶函式,不會因為 count 變化而重新建立
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);

return (
<div>
<p>點擊次數:{count}</p>
<ChildComponent data={data} onClick={handleClick} />
</div>
);
}

檢核點

  • ChildComponent 是否用 React.memo 包裹?
  • data 物件是否用 useMemo
  • onClick 是否用 useCallback

範例二:列表虛擬化

// ListView.jsx
import React from 'react';
import { FixedSizeList as List } from 'react-window';

const Row = React.memo(({ index, style }) => <div style={style}>列表項目 #{index}</div>);

export default function ListView() {
return (
<List height={400} itemCount={10000} itemSize={35} width={'100%'}>
{Row}
</List>
);
}

檢核點

  • 是否針對長列表導入虛擬化?
  • itemSize 與 height 設定是否合理?

範例三:Code Splitting 與懶載入

// routes.jsx
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));

export default function AppRouter() {
return (
<Router>
<Suspense fallback={<div>載入中...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/dashboard" component={Dashboard} />
</Switch>
</Suspense>
</Router>
);
}

檢核點

  • 是否有針對路由或大型元件進行懶載入?
  • fallback UI 是否友善?

範例四:效能分析

import React, { Profiler } from 'react';
import TodoList from './TodoList';

function onRenderCallback(id, phase, actualDuration) {
console.log(`${id} ${phase} 耗時:${actualDuration.toFixed(2)}ms`);
}

export default function App() {
return (
<Profiler id="TodoList" onRender={onRenderCallback}>
<TodoList />
</Profiler>
);
}

檢核點

  • 是否使用 Profiler 區隔並記錄核心元件耗時?
  • 是否定期檢視開發者工具數據?

總結

以上檢核清單涵蓋了從程式撰寫、元件切分,到效能分析與持續監控的各個面向。建議將此清單整合至 Pull Request 模板中,並在團隊中推廣效能優化文化。持續在日常開發中落實這些檢查,能確保應用在功能增長的同時仍保持流暢的使用者體驗,並降低潛在的性能退化風險。若需將本文轉為 Markdown、PDF 或 Notion 模板,歡迎隨時提出。

補充:React 效能優化 SOP 檢核清單

一、避免不必要的重新渲染

  • 是否使用 React.memo 包裹純函式元件?
  • 是否有使用 useCallback 記憶傳遞的函式 props?
  • 是否有使用 useMemo 記憶計算結果,避免重複計算?
  • 是否避免在 JSX 中直接宣告函式或物件(例如 inline style)?

二、State 管理與元件分離

  • 是否將狀態下放至最小影響範圍的元件中?
  • 是否避免使用不必要的 lifting state up?
  • 是否將 UI 狀態(如開關、hover 狀態)與全域狀態分離?

三、Props 傳遞與結構優化

  • 是否控制 props 深層傳遞導致的層層 re-render?
  • 是否 props 結構穩定、可預期?(避免 object/array 每次都變)

四、渲染大量資料時的處理

  • 是否針對長列表使用虛擬化工具(如 react-window, react-virtualized)?
  • 是否有合理使用 key(且為穩定值,例如 id 而非 index)?

五、資源載入與 Code Splitting

  • 是否使用 React.lazy + Suspense 實現元件懶載入?
  • 是否進行 route-based code splitting(使用動態 import)?
  • 是否有壓縮圖片、延遲圖片載入(lazy loading)?

六、效能監控與分析

  • 是否使用 React DevTools Profiler 檢查 render 頻率與時間?
  • 是否分析過 Lighthouse / Web Vitals 的效能指標?
  • 是否檢查 Bundle Size(Webpack 分析工具、SourceMap Explorer 等)?

七、避免常見陷阱

  • 是否避免每次 render 都新建匿名函式?
  • 是否避免重複 render 同一資料來源?
  • 是否避免過度依賴 context 導致全頁重 render?

八、開發階段優化習慣

  • 是否將效能優化納入 Code Review 檢查點?
  • 是否每個大型新元件都確認是否會引起不必要渲染?
  • 是否測試過主流程在弱網或低效能設備上的表現?

Lodash 介紹與入門教學筆記 | 學習筆記

· 閱讀時間約 3 分鐘
kdchang

前言

在 JavaScript 的開發中,資料處理與函式操作經常需要處理陣列、物件、字串的轉換、搜尋與過濾等需求。儘管 ES6+ 提供了不少內建函式(如 mapfilterreduce 等),但仍有許多情境需要進階或更簡潔的處理方式。

這時候,Lodash 就是一個非常實用的工具庫。它是一個提供大量實用函式的 JavaScript 函式庫,能幫助開發者更方便地進行資料操作、提升開發效率與可讀性。


重點摘要

  • Lodash 是什麼:

    • 一個現代 JavaScript 實用工具庫,專注於資料處理、陣列與物件操作。
    • 模組化設計,可依照需求引入特定函式,減少最終 bundle 大小。
  • 使用方式:

    • 安裝方式:

      npm install lodash
    • 引入方式(使用 ES6 模組):

      import _ from 'lodash';
  • 常用函式分類:

    • 陣列操作:chunkcompactdifferenceuniqflatten
    • 物件操作:getsetmergepickomit
    • 函式處理:debouncethrottleonce
    • 數學與邏輯判斷:isEmptyisEqualclamp
    • 字串操作:camelCasekebabCasestartCase
  • 優點:

    • API 設計一致,學習曲線平緩
    • 可與原生 JS 無縫搭配
    • 處理巢狀資料與深層結構特別方便

實際範例

以下透過幾個實際範例來展示 Lodash 的常見使用情境與語法。

1. 陣列切分:_.chunk

將一個陣列依照固定大小切成多個子陣列。

import _ from 'lodash';

const arr = [1, 2, 3, 4, 5, 6];
const result = _.chunk(arr, 2);
// 輸出:[[1, 2], [3, 4], [5, 6]]

2. 去除 falsy 值:_.compact

移除陣列中的 falsenull0""undefinedNaN

const arr = [0, 1, false, 2, '', 3];
const result = _.compact(arr);
// 輸出:[1, 2, 3]

3. 陣列差集:_.difference

找出第一個陣列中,不存在於其他陣列的元素。

const result = _.difference([1, 2, 3, 4], [2, 3]);
// 輸出:[1, 4]

4. 去除重複值:_.uniq

回傳一個不含重複值的新陣列。

const result = _.uniq([2, 1, 2]);
// 輸出:[2, 1]

5. 平坦化陣列:_.flatten

將多維陣列的一層扁平化。

const result = _.flatten([1, [2, [3, [4]]]]);
// 輸出:[1, 2, [3, [4]]]

若要完全扁平化,可使用 flattenDeep

const result = _.flattenDeep([1, [2, [3, [4]]]]);
// 輸出:[1, 2, 3, 4]

6. 取得物件巢狀值:_.get

避免使用多層 obj && obj.prop 判斷。

const obj = { a: { b: { c: 42 } } };
const result = _.get(obj, 'a.b.c');
// 輸出:42

可設定預設值:

const result = _.get(obj, 'a.b.d', 'not found');
// 輸出:'not found'

7. 防抖動(Debounce):_.debounce

常用於輸入框搜尋防抖,例如搜尋建議:

const search = _.debounce((query) => {
console.log('搜尋中:', query);
}, 300);

search('a');
search('ab');
search('abc'); // 只會觸發這次

8. 函式執行一次:_.once

保證某個函式只執行一次。

const init = _.once(() => {
console.log('只會執行一次的初始化');
});

init();
init();
// 輸出:只會執行一次的初始化

總結

Lodash 是一個穩定、完整、社群活躍的 JavaScript 工具函式庫,適合用於各種日常資料處理場景。尤其當面對資料轉換、巢狀結構處理、函式優化(如 debounce/throttle)等問題時,Lodash 提供了直觀且一致的解法。

若在專案中引入 Lodash 時,建議採用模組化方式僅引入需要的函式,或使用 lodash-es(es module 版本) 搭配 Tree Shaking,以減少最終輸出大小。

Tree Shaking 介紹與入門教學筆記 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

前言

在現代前端開發中,專案的程式碼結構越來越複雜,為了提高效能與使用者體驗,減少最終打包後的 JavaScript 檔案大小成為一項重要任務。這時,「Tree Shaking」技術便扮演了關鍵角色。

Tree Shaking 是一種靜態程式碼分析技術,能夠在打包階段分析模組之間的依賴關係,移除未被實際使用的程式碼(dead code)。這不僅能優化網站載入速度,也讓程式碼更精簡、維護性更高。


重點摘要

  • Tree Shaking 是什麼:

    • 靜態分析 JavaScript 模組的依賴關係,排除未使用的導出(export)。
    • 減少 bundle 體積,提升網站效能與載入速度。
  • 依賴條件:

    • 必須使用 ES Moduleimport / export),不適用於 CommonJS(require / module.exports)。
    • 模組必須 無副作用(side effects)
    • 打包工具必須支援 Tree Shaking(如 Webpack、Rollup、Vite)。
  • 支援工具:

    • Webpack:需設定 mode: 'production',可配合 sideEffects 設定。
    • Rollup:原生支援 Tree Shaking。
    • Vite:基於 Rollup,自然具備支援能力。
  • 副作用(Side Effects):

    • 當模組在載入時就執行會對外部環境產生影響的操作(如改寫全域物件、注入 CSS),即視為有副作用。
    • 若模組被標示為有副作用,Tree Shaking 不會移除它,即使未被使用。

實際範例

以下範例展示如何使用 Tree Shaking 與如何正確設定專案來支援這項技術。

範例 1:錯誤導入方式,導致無法 Tree Shaking

// main.js
import _ from 'lodash';

const result = _.uniq([1, 2, 2, 3]);

使用 import _ from 'lodash' 會引入整個 Lodash 函式庫,即使你只使用了 uniq 一個函式,打包結果也包含全部模組。


範例 2:正確方式,搭配 Tree Shaking 使用

npm install lodash-es
// main.js
import { uniq } from 'lodash-es';

const result = uniq([1, 2, 2, 3]);

lodash-es 是 Lodash 的 ES 模組版本,允許 Tree Shaking。打包工具會自動排除未使用的其他函式,例如 cloneDeepmerge 等,顯著降低檔案體積。


範例 3:Webpack 基礎設定

// webpack.config.js
module.exports = {
mode: 'production', // 啟用 Tree Shaking 與壓縮
optimization: {
usedExports: true, // 標記已使用的模組導出(production 模式下自動啟用)
},
};
// package.json
{
"sideEffects": false
}

這樣的設定告訴 Webpack 整個專案皆無副作用,因此可放心進行模組的 Tree Shaking。

若有些檔案需保留副作用(如樣式導入),可以用陣列方式指定:

{
"sideEffects": ["./src/styles.css"]
}

範例 4:Rollup 自動 Tree Shaking

Rollup 原生支援 Tree Shaking,只需使用 ES Module 即可:

// rollup.config.js
import { defineConfig } from 'rollup';
import babel from '@rollup/plugin-babel';

export default defineConfig({
input: 'src/main.js',
output: {
file: 'dist/bundle.js',
format: 'esm',
},
plugins: [babel({ babelHelpers: 'bundled' })],
});

Rollup 會分析哪些函式實際被使用,未使用的會自動排除。


建議實務策略

  • 使用模組化的函式庫版本,如 lodash-esdate-fns(每個功能一個函式)。
  • 避免使用 CommonJS 套件或 require() 語法,否則無法靜態分析。
  • 定期分析 bundle 體積,使用工具如 Webpack Bundle Analyzer
  • 結合動態 import,搭配 lazy loading 拆分 chunk 可進一步優化效能。

補充副作用(side effects)

在 JavaScript 中,副作用(side effects) 指的是 當程式碼執行時,對外部環境造成改變或依賴外部狀態 的行為。副作用是 Tree Shaking 技術的一大考量點,因為具有副作用的程式碼通常無法被安全地移除,即使它沒有被「明確使用」。


一、什麼是副作用?

常見的副作用行為包括:

  • 修改全域變數或物件
  • DOM 操作
  • 寫入檔案 / 本地儲存
  • 發送 API 請求
  • 設定計時器(setTimeout / setInterval
  • console.log、alert 等印出操作
  • 匯入會執行即時副作用的模組(例如樣式、polyfill)

二、舉例說明

✅ 無副作用(pure function)

export function add(a, b) {
return a + b;
}

這個函式純粹根據輸入產出結果,不會影響其他程式碼,可被安全地移除或優化。


❌ 有副作用

console.log('載入模組時執行'); // 印出訊息就是副作用

document.body.style.backgroundColor = 'black'; // 操作 DOM

export function add(a, b) {
return a + b;
}

即使 add() 函式未使用,只要這個模組被 import,就會執行 console.logdocument 操作,因此打包工具不敢移除它(怕破壞行為)。


三、Tree Shaking 為什麼在意副作用?

因為 Tree Shaking 的目標是 移除未使用的程式碼,但:

  • 如果一段程式碼 可能有副作用,打包工具 不敢隨便刪除,怕造成功能錯誤。
  • 所以需要明確標示模組是否有副作用。

四、如何告訴打包工具副作用資訊?

在專案或套件的 package.json 加入:

{
"sideEffects": false
}

代表:整個專案或套件中,所有模組都無副作用,Webpack/Rollup 就可以放心 Tree Shake。

若某些檔案(如樣式)確實有副作用,可以指定:

{
"sideEffects": ["./src/styles.css"]
}

五、實務建議

  • 撰寫可預測的 純函式(Pure Function),避免模組執行就改變外部狀態。
  • 匯入函式時盡量使用模組化方式,例如只 import 使用到的功能。
  • 避免使用模組中會「立即執行」某些操作的套件,若無法避免,務必在 sideEffects 中設定。

副作用本身並不是壞事,許多實用功能(像是發送請求、改變頁面樣式)本來就需要副作用。但在做效能優化時,了解副作用的存在與影響是 Tree Shaking 成功的關鍵

若模組沒有副作用,且未被使用,就可以安全地刪除。這就是 Tree Shaking 的核心機制。

總結

Tree Shaking 是現代前端打包流程中的一項關鍵技術,對於大型應用或需要精細資源管理的專案尤其重要。透過正確地使用 ES 模組、無副作用模組設計與支援工具設定,開發者可以有效刪除未使用的程式碼,提升效能、加快載入速度,並優化使用者體驗。

在實務專案中,建議開發者持續追蹤打包結果,並使用具備良好模組結構與 Tree Shaking 支援的函式庫,以保持專案維持在最佳的資源狀態。

Jest 使用 ES Module 入門教學筆記 | 學習筆記

· 閱讀時間約 5 分鐘
kdchang

前言

在 JavaScript 開發中,ES Modules (ESM) 已成為標準。從 Node.js 12 開始,ESM 已獲得原生支援,而前端開發(如 React、Vue、Svelte 等框架)早已全面採用 ES Module。 然而,當我們使用 Jest 來撰寫與執行測試時,若要直接使用 ES Module,會遇到一些設定上的挑戰。本篇筆記將說明如何在專案中讓 Jest 正確執行 ES Module 程式碼,並透過實例展示操作流程。


1. Jest 是否支援 ES Module?

Jest 自 v27 版本開始實驗性支援 ES Module,到了 v28 以後更加穩定。但因為 Node.js 與 CommonJS、ESM 的處理邏輯不同,仍需要額外設定。

若你使用 ES Module(例如 .mjs 檔案、type: module),或是前端專案用 ES6 import / export,就必須進行相應的 Jest 設定。


2. 初始化專案與安裝 Jest

首先,我們建立一個 Node.js 專案:

mkdir jest-esm-demo
cd jest-esm-demo
npm init -y

接著安裝 Jest:

npm install --save-dev jest

重點!package.json 中加入 type: "module",讓整個專案採用 ES Module:

{
"name": "jest-esm-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.0.0"
}
}

3. 撰寫 ES Module 程式碼

假設我們有一個簡單的 加法模組 sum.mjs

// sum.mjs
export function sum(a, b) {
return a + b;
}

接著建立測試檔案 sum.test.mjs

// sum.test.mjs
import { sum } from './sum.mjs';

test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});

這時候直接執行 npm test 會報錯:

SyntaxError: Cannot use import statement outside a module

因為 Jest 預設使用 CommonJS,不認得 ES Module,需要額外設定。


4. 設定 Jest 支援 ES Module

方案一:使用 jest.config.js 並指定 transform

首先,安裝 Babeljest-esm-transformer 之類的工具。如果希望簡單一點,可以直接使用 Jest 的內建 ESM 模式(推薦)。

建立 jest.config.js

// jest.config.js
export default {
transform: {},
extensionsToTreatAsEsm: ['.mjs'],
testEnvironment: 'node',
};

重點設定解釋:

  • transform: {} 表示不使用 Babel 或其他轉譯器
  • extensionsToTreatAsEsm 告訴 Jest 哪些副檔名視為 ESM
  • testEnvironment: 'node' 使用 Node 環境(非 jsdom)

執行 npm test,這時候仍會報錯:

Jest encountered an unexpected token

因為 Jest 內建 transform 無法處理 ES Module 語法(即便 Node.js 支援,但 Jest 內部解析流程不同)。


5. 方案二:使用 babel-jest 轉譯 ESM

安裝 Babel 及相關套件:

npm install --save-dev @babel/preset-env babel-jest

建立 .babelrc

{
"presets": ["@babel/preset-env"]
}

更新 jest.config.js

export default {
transform: {
'^.+\\.m?js$': 'babel-jest',
},
extensionsToTreatAsEsm: ['.mjs'],
testEnvironment: 'node',
};

這樣 Jest 會用 babel-jest 處理 .js.mjs 檔案,並當作 ESM 解析。

此時執行 npm test,測試就會通過:

PASS  ./sum.test.mjs
✓ adds 1 + 2 to equal 3 (5 ms)

6. 測試更複雜情境

假設我們有一個 fetch 模組,使用 async function:

// fetchData.mjs
export async function fetchData() {
return 'peanut butter';
}

測試檔:

// fetchData.test.mjs
import { fetchData } from './fetchData.mjs';

test('returns peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});

一樣執行 npm test,因為已經設定好 Babel 與 ESM,非同步測試也能正常運作。


7. 使用 import.meta.url 注意事項

如果你的程式中有用 import.meta.url(例如讀取檔案、動態載入),要注意 Jest 執行時 context 與 Node.js 直跑不同

例如:

// fileUtil.mjs
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export async function readConfig() {
const filePath = join(__dirname, 'config.json');
const content = await readFile(filePath, 'utf-8');
return JSON.parse(content);
}

測試時要確保 import.meta.url 能被正確解析(可能需要 jest-environment-node,或 Mock 檔案路徑)。


8. 常見錯誤與解法

問題:SyntaxError: Cannot use import statement outside a module

  • 確認 jest.config.js 中有 extensionsToTreatAsEsm
  • 測試檔、副程式檔是否副檔名為 .mjs

問題:Unexpected token export

  • 確認有設定 transform 使用 babel-jest
  • 確認 .babelrc 正確啟用 @babel/preset-env

問題:ESM 測試檔找不到 module

  • 確認 package.jsontype: module
  • 相對路徑記得補 .mjs 副檔名

9. 實用 Jest CLI 指令

  • 執行單一測試檔:
npx jest sum.test.mjs
  • 只跑某個測試:
test.only('專跑這個測試', () => { ... });
  • 顯示覆蓋率:
npx jest --coverage

總結

Jest 預設是為 CommonJS 設計,但隨著 ES Module 在 Node.js 與前端日漸普及,支援 ESM 變得越來越重要。

透過本文介紹,我們可以知道:

  1. type: module 啟用 ESM
  2. Jest 需要設定 transformextensionsToTreatAsEsm
  3. 使用 babel-jest 來處理 ESM
  4. 注意 import.meta.url、路徑、非同步等 ESM 細節

雖然設定比 CJS 稍微複雜,但一旦設定好後,整個測試流程一樣流暢,也能為未來更符合現代標準的專案奠定基礎。

建議未來若有使用 TypeScript、React、Vue 等,也可以結合對應的 transformer 與設定,讓 Jest 完全支援你的開發。

Jest AAA 測試原則入門教學筆記 | 學習筆記

· 閱讀時間約 2 分鐘
kdchang

範例測試:saveMoney 方法

atm.js

class ATM {
constructor(balance) {
this.balance = balance;
}
saveMoney(amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
this.balance += amount;
return this.balance;
}
withdrawMoney(amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
if (amount > this.balance) {
throw new Error('Insufficient balance');
}
this.balance -= amount;
return this.balance;
}
getBalance() {
return this.balance;
}
reset() {
this.balance = 0;
}
setBalance(balance) {
if (balance < 0) {
throw new Error('Balance cannot be negative');
}
this.balance = balance;
}
}

export default ATM;

atm.test.js

import ATM from '../atm.js';

test('saveMoney adds money to balance', () => {
// Arrange: 建立一個 ATM 實例,初始餘額 0
const atm = new ATM(0);

// Act: 存入 1000
atm.saveMoney(1000);

// Assert: 餘額應該變成 1000
expect(atm.balance).toBe(1000);
});

逐步對應:

步驟內容
Arrange建立 ATM 物件並給初始值
Act呼叫 saveMoney(1000)
Assert驗證 atm.balance 是否等於 1000

另一個範例:檢查錯誤拋出

如果要測試當金額是負數時會丟錯誤:

test('saveMoney throws error when amount <= 0', () => {
// Arrange
const atm = new ATM(0);

// Act & Assert
expect(() => atm.saveMoney(0)).toThrow('Amount must be positive');
});

這裡因為 actassert 綁在一起,所以在 expect 裡包了一個 function,來驗證是否拋出錯誤。


完整測試檔(用 AAA 標註)

import ATM from '../atm.js';

test('saveMoney adds money to balance', () => {
// Arrange
const atm = new ATM(0);

// Act
atm.saveMoney(1000);

// Assert
expect(atm.balance).toBe(1000);
});

test('saveMoney throws error when amount <= 0', () => {
// Arrange
const atm = new ATM(0);

// Act & Assert
expect(() => atm.saveMoney(0)).toThrow('Amount must be positive');
});

Google Lighthouse 介紹與入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

一、什麼是 Lighthouse?

Google Lighthouse 是 Google 開發的開源自動化工具,主要用來評估網頁的品質,包含 效能 (Performance)、無障礙 (Accessibility)、最佳化 (Best Practices)、SEO、漸進式網頁應用 (PWA) 等五大面向。透過 Lighthouse,開發者可以快速找到網站問題與優化建議,幫助網站在使用者體驗與搜尋引擎上表現更好。

Lighthouse 可以透過以下方式執行:

  • Chrome DevTools(瀏覽器內建)
  • Node.js CLI(命令列工具)
  • Lighthouse CI(持續整合工具)
  • Web 版https://pagespeed.web.dev/)

本教學以 Chrome DevTools 為主,搭配 命令列工具輔助說明。


二、如何在 Chrome 使用 Lighthouse

1. 開啟 Chrome DevTools

  • 使用 Chrome 瀏覽器打開我們想分析的網站
  • 按下 F12Ctrl+Shift+I (Mac: Cmd+Option+I) 開啟 DevTools
  • 點選 「Lighthouse」 分頁(如果沒有看到,點選 >> 更多選項即可)

2. 設定 Lighthouse 報告

在 Lighthouse 分頁中,可以看到幾個選項:

  • Categories:選擇要測試的項目(預設全選)
  • Device:選擇模擬裝置(Mobile 或 Desktop)

一般來說,建議從 Mobile 開始測試,因為 Google 搜尋主要使用行動端指標作為排名依據。

3. 開始產生報告

設定好後,點擊 「Analyze page load」(或「Generate report」),Lighthouse 會開始分析。分析過程會自動重新載入頁面並執行模擬測試,過程大約 30 秒至 1 分鐘。

完成後,會生成一份報告,包含分數、每個項目的問題說明與建議。


三、報告解讀與優化建議

以下為報告中幾個重要指標:

  1. Performance(效能)

    • First Contentful Paint (FCP):第一次內容繪製時間
    • Largest Contentful Paint (LCP):主要內容繪製完成時間
    • Time to Interactive (TTI):頁面可互動時間
    • Cumulative Layout Shift (CLS):累積版面位移

優化方向範例

  • 壓縮圖片(使用 WebP)
  • 延遲非必要 JavaScript 載入(lazy loading)
  • 使用 CSS/JS minify 工具
  • 啟用瀏覽器快取 (cache)
  1. Accessibility(無障礙)

    • 圖片是否有 alt 屬性
    • 表單元素是否有 label
    • 按鈕是否有可辨識的名稱

優化方向範例

  • 確保所有互動元件有適當的 ARIA 標籤
  • 保持足夠的色彩對比
  1. Best Practices(最佳化)

    • 是否使用 HTTPS
    • 是否避免過時的 API
    • 檢查瀏覽器安全設定
  2. SEO

    • 是否有 meta 描述
    • 是否設定 <title>
    • 是否有 robots.txt
    • 頁面是否可被索引

四、實際範例:分析一個網頁

前往 https://example.com 網站,操作步驟如下:

使用 Chrome DevTools:

  1. 用 Chrome 瀏覽 https://example.com
  2. 開啟 DevTools → Lighthouse 分頁
  3. 選擇 Mobile + 全部 Categories
  4. 點擊 Analyze page load

執行後,我們會看到一份報告,例如:

CategoryScore
Performance68
Accessibility92
Best Practices85
SEO90

針對效能分數 68,Lighthouse 會提出具體建議,例如:

  • "Serve images in next-gen formats" → 建議將 JPG/PNG 圖片轉換為 WebP
  • "Eliminate render-blocking resources" → 建議將 CSS/JS 非同步或延遲載入

此時,我們可以採取以下修正:

  • 使用 ImageMagickSquoosh 等工具壓縮並轉換圖片
  • 加上 <link rel="preload"> 標籤預先載入必要資源
  • script 標籤加上 defer 屬性

五、使用命令列執行 Lighthouse

如果需要自動化測試或整合到 CI/CD,可以用 Node.js 安裝 Lighthouse:

1. 安裝

npm install -g lighthouse

2. 執行

lighthouse https://example.com --view

執行後會產生一個 HTML 報告並自動打開。

可以用額外參數調整輸出:

lighthouse https://example.com --output json --output html --output-path ./report.html --preset desktop

六、實務應用與建議

  • 開發階段就導入:開發過程中就應該多次使用 Lighthouse,而不是到上線前才檢查。
  • 設定目標分數:通常建議 Mobile 效能達到 80 分以上。
  • 結合 CI/CD:用 Lighthouse CI 在部署時自動檢查網站品質,確保每次更新不會退步。

如果是大型專案,也可以與 WebPageTest、PageSpeed Insights 搭配,取得更廣泛的性能數據。


七、總結

Google Lighthouse 是一個功能強大的網站品質檢測工具,不僅能協助提升效能,還能兼顧 SEO、無障礙與最佳實務。無論是初學者或資深前端工程師,都建議將 Lighthouse 納入開發流程中,定期檢查與優化,為網站帶來更好的使用者體驗與搜尋排名。

透過本篇教學,相信我們已能夠:

  1. 知道如何使用 Chrome DevTools 產生報告
  2. 理解報告中指標與優化方式
  3. 用命令列執行 Lighthouse 以自動化分析

後續可以根據團隊需求,進一步探索 Lighthouse CI、API 或與其他性能工具整合的進階應用。

Progressive Web App(PWA)入門教學筆記 | 學習筆記

· 閱讀時間約 4 分鐘
kdchang

一、前言

在行動裝置普及的今天,使用者越來越期待 Web 應用程式能提供與原生 App 相近的使用體驗。然而,傳統 Web 應用程式在離線支援、效能與通知推送等方面仍有所不足。為了彌補這些缺點,Progressive Web App(PWA) 應運而生。

PWA 結合了 Web 技術與原生 App 的優勢,使網站具備離線可用、快速載入、可安裝、推播通知等功能,進而提升使用者體驗與互動黏著度。對開發者而言,PWA 是一種較低成本就能達到跨平台效果的解法。本文將帶你快速認識 PWA 的核心概念與實作方式,適合初學者或 Web 開發者作為入門參考。


二、重點摘要

  • PWA 定義:一種利用現代 Web API 提供類似原生 App 體驗的 Web 應用程式。

  • 三大核心技術

    • HTTPS:保護傳輸安全並啟用 Service Worker。
    • Web App Manifest:描述應用程式的基本資訊,例如名稱、圖示、顯示模式等。
    • Service Worker:一種可攔截網路請求並實現快取、離線功能的背景腳本。
  • PWA 特性

    • 可安裝(Installable)
    • 離線可用(Offline capable)
    • 背景推播(Push Notification)
    • 響應式設計(Responsive)
    • 快速載入(Fast loading)
  • 使用情境

    • 新創或中小企業開發跨平台 App
    • 強化使用者體驗的內容網站或工具型網站
    • 電商網站提升轉換率與留存

三、實際範例:打造一個簡單的 PWA

我們將從零開始建立一個基本的 PWA 網站,具備可安裝與離線快取功能。

1. 建立專案目錄與基本檔案

建立一個資料夾,內含下列檔案:

pwa-demo/
├── index.html
├── app.js
├── service-worker.js
├── manifest.json

2. 撰寫 index.html

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PWA Demo</title>
<link rel="manifest" href="manifest.json" />
</head>
<body>
<h1>Hello, PWA!</h1>
<script src="app.js"></script>
</body>
</html>

3. 撰寫 manifest.json

{
"name": "PWA Demo",
"short_name": "PWADemo",
"start_url": "./index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

請準備對應的 icon-192.pngicon-512.png 圖示檔案放入根目錄。

4. 撰寫 app.js 並註冊 Service Worker

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('service-worker.js')
.then((reg) => {
console.log('Service Worker 註冊成功:', reg.scope);
})
.catch((err) => {
console.log('Service Worker 註冊失敗:', err);
});
});
}

5. 撰寫 service-worker.js

const CACHE_NAME = 'pwa-demo-cache-v1';
const urlsToCache = ['/', '/index.html', '/app.js', '/icon-192.png', '/icon-512.png'];

self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)));
});

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => response || fetch(event.request))
);
});

6. 本地測試與部署

PWA 需在 HTTPS 或 localhost 環境下運行,因此建議使用以下方式測試:

npx serve

或是使用 VS Code 的 Live Server 插件。


四、補充建議與工具

  • Lighthouse:使用 Chrome DevTools 的 Lighthouse 工具分析網站是否符合 PWA 標準。
  • Workbox:Google 提供的套件,簡化 Service Worker 撰寫與管理。
  • Vite / Next.js:這些框架和建造工具支援套件快速啟用 PWA,例如 Vite Plugin PWA。

五、總結

Progressive Web App 是 Web 應用向原生體驗邁進的重要里程碑。透過簡單的設計與實作,我們可以為網站加入離線能力、安裝功能與更佳的效能表現。PWA 不僅提升使用者體驗,也為開發者帶來更高的轉換率與技術可能性。若你正考慮提升網站互動性與可達性,不妨從簡單的 PWA 嘗試開始,逐步拓展應用場景與技術深度。