jQuery 效能優化

雖然 jQuery 讓開發更加便利,但不當的使用方式可能會導致效能問題。本篇介紹如何撰寫更有效率的 jQuery 程式碼。

選擇器優化

ID 選擇器最快

ID 選擇器直接對應到原生的 document.getElementById(),是最快的選擇方式:

// 最快
$('#content');

// 不必要的限定,反而更慢
$('div#content');

避免萬用選擇器

萬用選擇器會遍歷所有元素,效能很差:

// 避免
$('*');
$('.container > *');

// 改用具體的選擇器
$('.container > div');

從右到左解析

瀏覽器解析 CSS 選擇器是從右到左,因此右邊的選擇器越具體越好:

// 較慢 - 先找所有 a,再過濾
$('.menu a');

// 較快 - 先用 ID 縮小範圍
$('#menu a');

// 更好 - 給連結加上 class
$('.menu-link');

縮小搜尋範圍

使用 find() 或第二個參數來縮小搜尋範圍:

// 搜尋整個文件
$('.item');

// 只在特定範圍內搜尋
$('#container').find('.item');

// 或使用第二個參數(context)
$('.item', '#container');

// 如果已經有 jQuery 物件,使用 find() 更好
var $container = $('#container');
$container.find('.item');

避免過度具體

過度具體的選擇器反而更慢且難以維護:

// 避免 - 過度具體
$('div.container ul.list li.item a.link');

// 改用
$('.item .link');
// 或直接
$('.item-link');

快取 jQuery 物件

避免重複選取

每次使用 $() 都會執行 DOM 查詢,應該將結果快取起來:

// 不好 - 重複查詢
$('.item').addClass('active');
$('.item').css('color', 'red');
$('.item').fadeIn();

// 好 - 快取結果
var $items = $('.item');
$items.addClass('active');
$items.css('color', 'red');
$items.fadeIn();

// 更好 - 使用鏈式呼叫
$('.item').addClass('active').css('color', 'red').fadeIn();

命名慣例

使用 $ 前綴表示 jQuery 物件:

var $header = $('#header');
var $items = $('.item');
var $submitBtn = $('#submit');

// 這樣可以一眼看出是 jQuery 物件
$header.addClass('fixed');

在閉包中快取

(function () {
  // 快取常用元素
  var $window = $(window);
  var $document = $(document);
  var $body = $('body');

  $window.on('scroll', function () {
    // 使用快取的 $window
    var scrollTop = $window.scrollTop();
    // ...
  });
})();

DOM 操作優化

批量操作

避免在迴圈中逐一操作 DOM,應該先組合好再一次插入:

// 不好 - 每次迴圈都操作 DOM
var $list = $('#list');
for (var i = 0; i < 100; i++) {
  $list.append('<li>項目 ' + i + '</li>');
}

// 好 - 先組合字串,再一次插入
var html = '';
for (var i = 0; i < 100; i++) {
  html += '<li>項目 ' + i + '</li>';
}
$('#list').append(html);

// 或使用陣列
var items = [];
for (var i = 0; i < 100; i++) {
  items.push('<li>項目 ' + i + '</li>');
}
$('#list').append(items.join(''));

使用 DocumentFragment

var fragment = document.createDocumentFragment();

for (var i = 0; i < 100; i++) {
  var li = document.createElement('li');
  li.textContent = '項目 ' + i;
  fragment.appendChild(li);
}

$('#list').append(fragment);

使用 detach() 減少 reflow

大量操作元素時,先將元素從 DOM 移除,操作完再放回:

var $list = $('#list');
var $parent = $list.parent();

// 從 DOM 移除(但保留資料和事件)
$list.detach();

// 進行大量操作
for (var i = 0; i < 1000; i++) {
  $list.append('<li>項目 ' + i + '</li>');
}

// 放回 DOM
$parent.append($list);

減少 reflow 和 repaint

// 不好 - 多次讀寫交替會導致多次 reflow
var $box = $('#box');
$box.css('width', '100px');
var height = $box.height(); // 強制 reflow
$box.css('height', height + 'px');

// 好 - 先讀取,再寫入
var $box = $('#box');
var height = $box.height(); // 讀取
$box.css({
  // 一次寫入
  width: '100px',
  height: height + 'px',
});

使用 CSS 類別而非行內樣式

// 不好 - 多次修改行內樣式
$('#box').css('color', 'red').css('font-size', '20px').css('background', 'yellow');

// 好 - 預先定義 CSS 類別
$('#box').addClass('highlight');

事件優化

使用事件委派

對於大量相似元素或動態新增的元素,使用事件委派:

// 不好 - 為每個元素綁定事件
$('.item').on('click', function () {
  // ...
});

// 好 - 事件委派
$('#container').on('click', '.item', function () {
  // ...
});

事件委派的好處:

  • 只需要一個事件處理函式
  • 動態新增的元素自動有效
  • 減少記憶體使用

適時解除綁定

不再需要的事件應該解除綁定:

// 使用命名空間方便解除
$('#btn').on('click.myFeature', handler);

// 之後解除
$('#btn').off('.myFeature');

// 元素移除前解除事件
$('#element').off().remove();

節流和防抖

對於高頻率觸發的事件(如 scroll、resize、input),使用節流或防抖:

// 節流 - 固定間隔執行一次
function throttle(fn, delay) {
  var lastCall = 0;
  return function () {
    var now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      fn.apply(this, arguments);
    }
  };
}

// 防抖 - 停止觸發後才執行
function debounce(fn, delay) {
  var timer;
  return function () {
    var context = this;
    var args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
  };
}

// 使用
$(window).on('scroll', throttle(handleScroll, 100));
$('#search').on('input', debounce(handleSearch, 300));

使用 one() 處理一次性事件

// 只執行一次
$('#btn').one('click', function () {
  // 初始化操作
});

動畫優化

CSS 動畫優於 jQuery 動畫

CSS 動畫由瀏覽器優化,效能更好:

.fade-in {
  animation: fadeIn 0.3s ease-in-out;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
// 使用 CSS 動畫
$('#box').addClass('fade-in');

// 而非 jQuery 動畫
$('#box').fadeIn(300);

使用 stop() 防止動畫堆疊

// 避免動畫堆疊
$('#box').stop().animate({ left: '100px' }, 300);

// hover 時特別重要
$('.item').hover(
  function () {
    $(this).stop().animate({ opacity: 1 }, 200);
  },
  function () {
    $(this).stop().animate({ opacity: 0.5 }, 200);
  }
);

避免同時動畫太多元素

// 不好 - 同時動畫 1000 個元素
$('.item').fadeIn(300);

// 好 - 分批或延遲
$('.item').each(function (index) {
  $(this)
    .delay(index * 50)
    .fadeIn(300);
});

關閉動畫(測試或效能考量)

// 全域關閉動畫
$.fx.off = true;

// 根據使用者偏好
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  $.fx.off = true;
}

Ajax 優化

快取 Ajax 結果

var cache = {};

function getData(url) {
  if (cache[url]) {
    return $.Deferred().resolve(cache[url]).promise();
  }

  return $.get(url).then(function (data) {
    cache[url] = data;
    return data;
  });
}

取消未完成的請求

var currentRequest = null;

$('#search').on('input', function () {
  var query = $(this).val();

  // 取消之前的請求
  if (currentRequest) {
    currentRequest.abort();
  }

  currentRequest = $.get('/api/search', { q: query })
    .done(function (data) {
      // 處理結果
    })
    .always(function () {
      currentRequest = null;
    });
});

其他優化建議

使用最新版 jQuery

新版本通常包含效能改進和 bug 修復。

使用壓縮版

生產環境使用 .min.js 版本:

<!-- 開發環境 -->
<script src="jquery.js"></script>

<!-- 生產環境 -->
<script src="jquery.min.js"></script>

使用 Slim 版本

如果不需要 Ajax 和動畫效果,可以使用 Slim 版本減少檔案大小:

<script src="jquery.slim.min.js"></script>

延遲載入非必要的腳本

<!-- 非必要的腳本放在頁面底部或使用 defer -->
<script src="jquery.min.js"></script>
<script src="app.js" defer></script>

避免使用 $.each() 處理大型陣列

原生迴圈更快:

var items = [
  /* 大量資料 */
];

// 較慢
$.each(items, function (index, item) {
  // ...
});

// 較快
for (var i = 0, len = items.length; i < len; i++) {
  var item = items[i];
  // ...
}

// 或使用 forEach
items.forEach(function (item, index) {
  // ...
});

使用效能分析工具

  • 瀏覽器開發者工具的 Performance 面板
  • Console 的 console.time()console.timeEnd()
console.time('選擇器測試');
for (var i = 0; i < 1000; i++) {
  $('#element');
}
console.timeEnd('選擇器測試');

效能檢查清單

項目建議
選擇器使用 ID 選擇器,避免萬用選擇器
快取快取重複使用的 jQuery 物件
DOM 操作批量操作,減少 reflow
事件使用事件委派,適時解除綁定
動畫優先使用 CSS 動畫,使用 stop()
Ajax快取結果,取消重複請求
載入使用壓縮版,延遲載入