본문으로 건너뛰기
wooncloud

캐시 효과를 재는데 캐시가 꺼져 있었다, Playwright route()의 함정

Playwright 는 route 를 하나라도 등록하면 그 컨텍스트의 HTTP 캐시를 끕니다. 캐시 재사용 효과를 재다가 warm 값이 cold 와 같게 나와서 CDP Fetch 로 바꾼 과정을 정리했습니다.

5분 분량

같은 출처 iframe이 부모 페이지가 이미 받아 둔 웹폰트를 재사용하도록 바꾼 뒤, 전후를 비교하려 했습니다. 배포 전이라 Playwright로 실제 서비스에 접속하고, iframe의 HTML과 JS 요청만 page.route()와 context.route()로 가로채 로컬 빌드로 바꿔치기했습니다.

개선은커녕 더 느려 보였다

결과가 이상했습니다. iframe이 부모와 같은 URL의 폰트(약 2MB)를 매번 처음부터 다시 받고 있었습니다. 개선이 없었을 뿐 아니라 오히려 느려 보였고, warm(두 번째 열기)이라고 잰 값도 사실상 cold였습니다.

원인은 코드가 아니라 측정 장치였습니다. Playwright는 route를 하나라도 등록하면 그 컨텍스트의 HTTP 캐시를 끕니다. 가로채지 않은 요청까지 전부요.

그러니 "캐시를 공유해서 빨라진다"는 가설은 이 방식으로는 원리적으로 측정할 수 없습니다.

CDP Fetch 로 직접 가로채기

가로채기를 Playwright route 대신 CDP Fetch 도메인으로 직접 걸었습니다. 이 경우 브라우저 캐시는 그대로 살아 있습니다. 몇 가지는 신경 써야 했습니다.

  • 패턴은 바꿔치기할 경로로만 좁힙니다. 나머지는 continueRequest로 실서버에 보냅니다.
  • 로컬 응답에는 크기에 비례한 지연(TTFB + 바이트/대역)을 넣습니다. 그러지 않으면 번들 크기 차이가 0ms로 보입니다.
  • 해시가 붙은 자산은 같은 실행 안에서 두 번째 요청부터 지연을 0으로 둬 캐시 적중을 흉내 냅니다. CDP로 fulfill한 응답은 HTTP 캐시에 저장되지 않기 때문입니다.
  • 비교 대상끼리 같은 자원을 같은 경로로 보냅니다. 한쪽 폰트는 로컬 fulfill(대역 경쟁 없음)로, 다른 쪽은 실네트워크로 보내면 공정하지 않습니다.
const cdp = await context.newCDPSession(page);
await cdp.send("Fetch.enable", {
  patterns: [{ urlPattern: "https://example.com/app/*", requestStage: "Request" }],
});
cdp.on("Fetch.requestPaused", async (ev) => {
  if (!shouldReplace(ev.request.url)) {
    return cdp.send("Fetch.continueRequest", { requestId: ev.requestId });
  }
  const body = readLocalBuild(ev.request.url);
  await delay(25 + wireSize(body) / 8000); // TTFB + 대역 보정
  await cdp.send("Fetch.fulfillRequest", {
    requestId: ev.requestId,
    responseCode: 200,
    responseHeaders: [{ name: "content-type", value: "text/javascript" }],
    body: body.toString("base64"),
  });
});

바꾼 뒤에는 폰트가 304 재검증(수백 바이트)으로 끝나는 게 보였고, cold 중앙값이 약 절반으로 줄었습니다.

다음부터는

캐시나 재사용 효과를 재는 실험이라면 route를 쓰는 순간 결과를 믿지 않기로 했습니다. warm 값이 cold와 비슷하게 나오면 측정 장치부터 의심하고, 요청 로그에서 fromCache와 304 여부를 직접 확인한 뒤에 숫자를 봅니다.

참고