Danh sách bài viết

Bài 125: Block Resources — Images, Fonts, Ads

Functional test không cần render đúng pixel — chỉ cần DOM đúng và logic đúng. Block image, font, media, analytics và ad request giúp test chạy nhanh hơn mà không làm thay đổi kết quả assert. Bài này trình bày các pattern block theo resourceType, theo URL domain, theo extension file, selective block chỉ third-party, context-level block cho toàn suite, trade-off giữa speed và coverage, và 4 pitfall thường gặp khi áp dụng block quá rộng.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài này, bạn sẽ:

  • Hiểu lý do block resource trong functional test và ước lượng mức độ cải thiện tốc độ.
  • Viết được pattern block theo resourceType: image, font, media.
  • Block ads và analytics theo URL domain mà không ảnh hưởng đến API của app.
  • Dùng regex extension để block nhanh mà không cần inspect từng request.
  • Phân biệt khi nào nên block và khi nào không nên (visual regression, lazy load test).
  • Áp dụng context-level block để không lặp lại setup trong từng test.
  • Tránh 4 pitfall phổ biến khi block quá rộng hoặc quên route.continue().
2

Tại Sao Block Resources

Khi Playwright load một trang, browser tải tất cả resource mà page yêu cầu: HTML, CSS, JS, hình ảnh, font, video, tracking beacon, ad script. Với functional test — kiểm tra logic form submit, navigation, API response — phần lớn các resource này không ảnh hưởng đến kết quả test.

Vấn đề phát sinh từ resource không cần thiết:

  • Tốc độ: Image và font thường chiếm 60–80% tổng byte của một trang. Skip chúng giúp page load nhanh hơn đáng kể — trong thực tế 30–50% faster cho functional test suite lớn.
  • Flakiness từ third-party: Analytics, ads, heatmap gọi ra ngoài domain. Các endpoint này có SLA thấp hơn API của bạn, đôi khi timeout hoặc trả error — gây test flaky mặc dù app không có bug.
  • CI cost: Bandwidth trên CI không miễn phí. Block binary asset giảm lượng dữ liệu tải về trong mỗi test run.
  • Focus: Functional test nên assert DOM và behavior, không phải chờ Cloudflare phân phối hình ảnh về.

Block resource không làm test kém tin cậy — miễn là bạn không block thứ mà test đang kiểm tra.

3

Pattern Block Theo resourceType

route.request().resourceType() trả về string phân loại nguồn gốc của request trong browser. Danh sách đầy đủ: document, stylesheet, image, media, font, script, texttrack, xhr, fetch, eventsource, websocket, manifest, other.

Pattern cơ bản — block image, font, media và để mọi thứ khác đi tiếp:

await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  if (['image', 'font', 'media'].includes(type)) {
    route.abort();
  } else {
    route.continue();
  }
});

Quan điểm chọn type nào để block:

  • image — ảnh từ <img>, <picture>, CSS background-image. Block được trong hầu hết functional test.
  • font — web font (.woff2, .ttf, .otf). Browser fallback về system font — text vẫn render, DOM không thay đổi.
  • media<video>, <audio>. Block nếu test không liên quan đến media playback.
  • stylesheet — CSS. Thường không nên block: CSS ảnh hưởng layout, element visibility, pseudo-element — nhiều assert và locator phụ thuộc.
  • script — JS. Không bao giờ block trong functional test trừ khi biết chính xác script nào safe.
  • document — HTML chính. Không block.

Lý do route.continue() trong nhánh else là bắt buộc: nếu bỏ qua, request không được fulfill và cũng không bị abort — browser treo chờ vô thời hạn. Xem Pitfall 3 bên dưới.

4

Pattern Block Ads và Analytics Theo Domain

Nhiều page load script từ third-party domain: Google Analytics, Google Tag Manager, DoubleClick, Facebook Pixel, Hotjar, Intercom, Segment. Những request này không liên quan đến logic của app, nhưng gây chậm hoặc flaky khi endpoint ngoài không ổn định.

Pattern block theo URL domain:

const blockedDomains = [
  'doubleclick.net',
  'google-analytics.com',
  'googletagmanager.com',
  'facebook.com/tr',
  'hotjar.com',
];

await page.route('**/*', (route) => {
  const url = route.request().url();
  if (blockedDomains.some(d => url.includes(d))) {
    route.abort();
  } else {
    route.continue();
  }
});

Pattern này block bất kỳ request nào có URL chứa domain trong danh sách. Nếu trang có nhiều tracking script khác nhau, chỉ cần thêm domain vào mảng — không cần thay đổi logic handler.

Một điểm quan trọng: block theo URL linh hoạt hơn block theo resourceType khi cần phân biệt script của bên thứ ba với script của app. resourceType === 'script' không phân biệt được script nào là internal và script nào là external analytics.

5

Pattern Block Theo Extension File

Thay vì inspect resourceType trong handler, có thể dùng URL pattern dạng regex trực tiếp trong page.route():

await page.route(
  /\.(png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf|otf|eot)$/,
  route => route.abort()
);

Ưu điểm: ngắn gọn, không cần route.continue() vì pattern chỉ match request cần block. Playwright không invoke handler này cho request không khớp regex — request đó đi thẳng qua.

Nhược điểm:

  • Extension không luôn đáng tin: API endpoint /api/avatar.png trả JSON metadata về ảnh — block sẽ làm app thiếu data, không phải thiếu ảnh.
  • URL với query string hoặc hash đôi khi không match regex nếu extension bị che khuất (/image?id=123&format=png match được, nhưng /cdn/img/abc không match dù thực tế trả PNG).
  • Không bao phủ resource được serve qua CDN với URL không có extension.

Pattern này phù hợp nhất khi bạn kiểm soát URL structure của app và biết image/font luôn có extension rõ ràng.

6

Combine Block: Type + URL

Trong thực tế, bạn thường muốn block cả hai: resource nặng theo type và third-party script theo URL. Combine trong một handler duy nhất:

await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  const url = route.request().url();

  if (
    ['image', 'font', 'media'].includes(type) ||
    url.includes('analytics') ||
    url.includes('doubleclick.net') ||
    url.includes('hotjar.com')
  ) {
    route.abort();
  } else {
    route.continue();
  }
});

Một handler cho toàn bộ rule block — dễ maintain hơn là đăng ký nhiều page.route() riêng lẻ. Khi có nhiều route handler match cùng một URL, Playwright invoke chúng theo thứ tự đăng ký ngược (last-in, first-served). Gộp logic vào một handler tránh sự phụ thuộc vào thứ tự đăng ký.

Nếu cần error code cụ thể để debug trong browser DevTools, truyền tường minh:

route.abort('blockedbyclient');

Chromium DevTools hiển thị reason này trong cột Status của Network panel, giúp phân biệt request bị block bởi test với request fail do lỗi thật.

7

Selective Block — Chỉ Third-Party

Tình huống: app có hình ảnh sản phẩm do chính server phục vụ (myapp.com/images/). Bạn muốn block ảnh CDN bên ngoài nhưng giữ ảnh sản phẩm vì test assert alt text và src attribute của chúng.

await page.route('**/*', (route) => {
  const url = route.request().url();
  const type = route.request().resourceType();

  // Block external image, giữ lại image của chính app
  if (type === 'image' && !url.includes('myapp.com')) {
    route.abort();  // block third-party image
  } else {
    route.continue();
  }
});

Pattern này cho phép kiểm soát ở mức domain: tất cả image từ domain không phải app đều bị block, image của app vẫn được load bình thường.

Mở rộng cho nhiều domain first-party (subdomain, CDN của chính app):

const firstPartyDomains = ['myapp.com', 'cdn.myapp.com', 'static.myapp.com'];

await page.route('**/*', (route) => {
  const url = route.request().url();
  const type = route.request().resourceType();

  const isFirstParty = firstPartyDomains.some(d => url.includes(d));

  if (['image', 'font'].includes(type) && !isFirstParty) {
    route.abort();
  } else {
    route.continue();
  }
});
8

Context-Level Block Cho Toàn Suite

Đăng ký block trên page chỉ áp dụng cho page đó. Nếu test mở nhiều tab hoặc nhiều test trong suite cần cùng block rule, dùng context.route() thay vì page.route():

// tests/fixtures.ts — fixture setup
import { test as base } from '@playwright/test';

export const test = base.extend({
  context: async ({ browser }, use) => {
    const context = await browser.newContext();

    await context.route('**/*', (route) => {
      const type = route.request().resourceType();
      if (['image', 'font', 'media'].includes(type)) {
        route.abort();
      } else {
        route.continue();
      }
    });

    await use(context);
    await context.close();
  },
});

Hoặc nếu không dùng custom fixture, đặt trong beforeEach:

test.beforeEach(async ({ context }) => {
  await context.route('**/*', (route) => {
    const type = route.request().resourceType();
    if (['image', 'font', 'media'].includes(type)) {
      route.abort();
    } else {
      route.continue();
    }
  });
});

context.route() áp dụng cho mọi page được tạo từ context đó — kể cả popup (page.waitForEvent('popup')) và new tab. Handler đăng ký trên context chạy trước handler đăng ký trên page khi cả hai cùng match.

Lưu ý hiệu năng: mỗi request đều phải qua handler để kiểm tra condition. Khi suite có hàng nghìn request, overhead này tích lũy nhỏ nhưng có đo được. Với block pattern đơn giản (chỉ kiểm tra resourceType), overhead thường không đáng kể so với lợi ích từ việc không tải binary asset.

9

Trade-Off: Khi Nào KHÔNG Block

Block resource không phải chiến lược phù hợp cho mọi loại test. Bảng so sánh:

Loại test Block image/font? Lý do
Functional (form, navigation, API) Nên block Image/font không ảnh hưởng kết quả assert
Visual regression (screenshot compare) Không block Block image → snapshot thiếu nội dung, diff sai
Test image lazy load Không block Test cần verify image request được gửi đúng lúc
Test font display / FOUT Không block Test cần font load để verify render behavior
Test layout shift (CLS) Không block Layout shift do image chưa load cần được đo thật
Performance test Không block Cần tải đầy đủ để đo thời gian thật

Thực hành tốt: tách file config hoặc fixture thành hai nhóm — một nhóm có block resource (functional test suite), một nhóm không có block (visual regression suite). Không mix trong cùng một context setup.

Ví dụ tách cấu hình:

// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'functional',
      use: { ...devices['Desktop Chrome'] },
      testMatch: 'tests/functional/**/*.spec.ts',
    },
    {
      name: 'visual',
      use: { ...devices['Desktop Chrome'] },
      testMatch: 'tests/visual/**/*.spec.ts',
      // không có block resource setup
    },
  ],
});

Block resource chỉ đặt trong fixture của project functional, project visual không kế thừa.

10

Pitfalls

Pitfall 1: Block stylesheet hoặc script — page broken

// SAI — block cả stylesheet làm page mất style
await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  // Thêm 'stylesheet' vào danh sách block để "tăng tốc thêm"
  if (['image', 'font', 'media', 'stylesheet'].includes(type)) {
    route.abort();
  } else {
    route.continue();
  }
});
// CSS bị block → element không visible, locator bị ẩn, assert thất bại
// vì selector dựa vào pseudo-class :not(.hidden) hay display: none

// ĐÚNG — chỉ block type an toàn
if (['image', 'font', 'media'].includes(type)) {
  route.abort();
}

Pitfall 2: Block trong visual regression — snapshot sai

// SAI — đặt block context ở global beforeEach, không phân biệt test type
test.beforeEach(async ({ context }) => {
  await context.route('**/*', (route) => {
    if (route.request().resourceType() === 'image') route.abort();
    else route.continue();
  });
});

// Test visual regression trong cùng file:
test('homepage screenshot', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png');
  // Snapshot chụp không có image → diff so với baseline có image → test fail sai
});

Pitfall 3: Quên route.continue() trong nhánh else — request treo

// SAI — thiếu else branch
await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  if (['image', 'font'].includes(type)) {
    route.abort();
  }
  // Không có route.continue() → tất cả request không phải image/font bị treo
  // Browser chờ response mãi mãi → test timeout sau 30s
});

// ĐÚNG — luôn có else với route.continue()
await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  if (['image', 'font'].includes(type)) {
    route.abort();
  } else {
    route.continue();
  }
});

Pitfall 4: Block first-party image mà test đang assert

// SAI — block toàn bộ image kể cả ảnh sản phẩm
await page.route('**/*', (route) => {
  if (route.request().resourceType() === 'image') {
    route.abort();
  } else {
    route.continue();
  }
});

// Test bên dưới sẽ fail vì image bị block
test('product image loads correctly', async ({ page }) => {
  await page.goto('/product/123');
  const img = page.locator('[data-testid="product-image"]');
  await expect(img).toHaveAttribute('src', /\/images\/product-123/);
  // src attribute vẫn có, nhưng nếu test còn assert naturalWidth > 0 thì fail
  // vì image không load
});

// ĐÚNG — selective block, giữ lại first-party image
await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  const url = route.request().url();
  if (type === 'image' && !url.includes('myapp.com')) {
    route.abort();  // chỉ block external image
  } else {
    route.continue();
  }
});
11

Quiz

Câu 1. Handler sau có vấn đề gì? Test sẽ hành xử như thế nào?

await page.route('**/*', async (route) => {
  const type = route.request().resourceType();
  if (type === 'image') {
    await route.abort();
  }
});
Đáp án

Thiếu else { route.continue(); }. Mọi request không phải image — bao gồm HTML, CSS, JS, fetch, XHR — không được xử lý. Playwright không tự động continue khi handler không gọi abort/fulfill/continue/fallback. Tất cả request non-image treo, browser không nhận response, test timeout hoặc page không load được.

Câu 2. Bạn muốn block image nhưng giữ lại ảnh product từ shop.myapp.com. Viết handler phù hợp.

Đáp án
await page.route('**/*', (route) => {
  const type = route.request().resourceType();
  const url = route.request().url();
  if (type === 'image' && !url.includes('shop.myapp.com')) {
    route.abort();
  } else {
    route.continue();
  }
});

Câu 3. Tại sao không nên block stylesheet trong functional test?

Đáp án

CSS ảnh hưởng đến display, visibility, opacity, z-index và pseudo-class. Nhiều locator và assertion dựa vào element phải visible (ví dụ: expect(locator).toBeVisible() fail nếu element bị CSS ẩn). Block stylesheet làm mất style → element có thể không hiển thị đúng → assert sai mà không phải do bug trong app.

Câu 4. Sự khác biệt giữa đăng ký block trên page.route()context.route() là gì? Khi nào dùng context?

Đáp án

page.route() chỉ áp dụng cho page đó. context.route() áp dụng cho mọi page trong context — kể cả popup và tab mới mở trong test. Dùng context.route() khi test có thể mở nhiều tab, hoặc khi muốn block rule áp dụng cho toàn bộ test trong suite mà không lặp lại setup trong từng test.

Câu 5. Handler dưới dùng regex pattern thay vì '**/*' với condition trong handler. Ưu và nhược điểm so với approach dùng resourceType() là gì?

await page.route(/\.(png|jpg|jpeg|gif|webp|svg|woff2?)$/, route => route.abort());
Đáp án

Ưu: Ngắn gọn, không cần route.continue() (handler chỉ được gọi với request match regex). Handler không được invoke cho request khác — không có overhead.

Nhược: Dựa vào URL extension, không bắt được image/font phục vụ qua URL không có extension (ví dụ: CDN URL dạng /cdn/asset/abc123). Cũng có thể false-positive block endpoint API có path kết thúc bằng extension (ví dụ: /api/export/report.png trả JSON).

12

Bài Tiếp Theo

Bài 126: HAR File Format — bắt đầu nhóm C.2 HAR record & replay: cấu trúc file HAR, cách Playwright ghi lại network traffic, và tại sao HAR là lựa chọn phù hợp khi cần replay toàn bộ network session thay vì mock từng endpoint.