Language/JavaScript

bfcache 알아보기

SambaLim 2023. 5. 1. 22:03

BFCache(Back-Foward Cache)는 브라우저의 뒤로, 앞으로 버튼을 사용할 때 페이지를 즉시 로드할 수 있도록 도와주는 역할을 합니다. 이는 느린 네트워크, 장치를 사용하는 사용자의 경험을 향상시켜줍니다. 따라서 사용자의 경험을 위해 bfcache 최적화에 대해 이해하는 것이 중요합니다.

bfcache란?

bfcache는 사용자가 다른 곳으로 이동할 때 JS의 힙 메모리 영역을 포함한 전체 스냅샷을 저장하는 캐시입니다. 이를 통해 사용자가 이전페이지로 돌아가고자 했을 때 빠르게 전체 페이지를 보여줄 수 있습니다.

말 그대로 스냅샷이기 때문에, 리소스를 다시 다운로드 할 필요가 없습니다. 네트워크 요청이 일어나지 않고 스크롤 위치 또한 복원해줍니다. Task Queue에 대기 중이었던 작업 (setTimeout, Promise)등도 보존되었다가 다시 실행되기도 합니다.

bfcache가 활성화되지 않은 경우

이전 페이지를 로드하기 위해 새 요청이 시작되고 해당 페이지가 반복 방문에 대해 얼마나 최적화되었는지에 따라 브라우저는 리소스의 일부 또는 전체를 다시 다운로드하고, 다시 구문 분석하고, 다시 실행해야 합니다.

bfcache가 활성화된 경우

이전 페이지를 즉각적으로 다시 로드합니다. 네트워크에 연결하지 않고도 전체 페이지를 메모리에서 복원할 수 있습니다.

bfcache 작동 방식

bfcache의 cache는 HTTP cache와 다르게 동작합니다. bfcache는 메모리에 있는 전체 페이지의 스냅샷을 캐싱하는 반면, HTTP cache는 이전에 작성된 요청에 대한 응답만 포함합니다. 따라서 페이지 로드하는 데 필요한 모든 요청이 HTTP cache에서 이행될 수 있는 경우는 매우 드물기 때문에 bfcache 복원을 사용하는 경우 더 빠르게 페이지를 로드할 수 있습니다.

위험성

bfcache는 메모리에 전체 페이지의 스냅샷을 캐싱하기 때문에 setTimeout과 같이 Task Queue에 대기중이었던 현재 실행중인 코드 또한 보존합니다. 브라우저가 보류중인 timer을 일시 중지하고 bfcache로 페이지를 복원했을 때 다시 실행하는데, 이는 매우 혼란스럽거나 예기치 않은 동작을 일으킬 수 있습니다.

bfcache를 관찰하는 api

bfcache는 브라우저가 자동으로 하는 최적화이지만, 개발자가 페이지를 최적화 하고 성능을 측정하고 조정할 수 있도록 bfcache의 동작을 이해하는 것이 중요합니다.

bfcache를 관찰하는데 주로 페이지 전환 이벤트(pageshow, pagehide)입니다. 이는 bfcache가 사용되는 경우 거의 모든 브라우저에서 지원해주었습니다.

 

PageTransitionEvent

새롭게 추가된 페이지 생명 주기 이벤트(freeze, resume)은 bfcache를 확인하는데 도움을 주지만, 최신의 Chromium기반 브라우저에서만 지원됩니다. 예를들어 freeze, resume을 사용하면 CPU 사용량을 최소화 하기 위하여 백그라운드 탭을 프리징할 때에 이 이벤트를 쓸 수 있습니다.

위험성

브라우저 종류 및 버전에 따라 bfcache의 사용여부를 알 수 없으며 캐시가 되는 상황이나 조건이 달라 예측이 어렵습니다. 또한 캐시가 메모리에 저장되어 있는 시간도 브라우저마다 달라 bfcache가 보장되지 않습니다.

PageTransitionEvent

Session history의 페이지가 현재 페이지가 아닌 경우 실행됩니다. pageshow, pagehide 이벤트를 포함합니다. 이를 통해 bfcache를 관찰할 수 있습니다.

pageshow

window.addEventListener('pageshow', (event) => {
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});

pageshow 이벤트는 페이지가 처음 로드될 때와 페이지가 bfcache에서 복원될 때 load 이벤트 직후에 발생합니다. pageshow 이벤트에는 persisted 속성이 있으며, 페이지가 bfcache에서 복원된 경우 true 이고 그렇지 않은 경우 false 입니다.

pageshow 이벤트 직전에 resume 이벤트가 발생하기도 하지만, 백그라운드에서 정지되었던 탭에서 다시 돌아왔거나 하는 경우도 발생하므로 bfcache를 관찰하기에는 적합하지 않습니다.

pagehide

window.addEventListener('pagehide', (event) => {
  if (event.persisted === true) {
    console.log('This page *might* be entering the bfcache.');
  } else {
    console.log('This page will unload normally and be discarded.');
  }
});

pagehide 도 persisted 속성을 가지고 있고 만약 persisted가 false라면 bfcache에 들어가지 않을 것이라고 확신할 수 있습니다. 하지만 true라면 페이지가 캐시된다는 보장이 없습니다.

유사하게 freeze이벤트는 pagehide이벤트 직후에 실행되지만(persisted 속성이 true인 경우) 이는 브라우저가 페이지를 캐시하려고 한다는 의미일 뿐, 캐시된다는 것을 보장하지는 못합니다.

bfcache로 페이지 최적화 하기

모든 페이지가 bfcache로 캐싱되는 것은 아니며 캐싱되더라도 저장되어있는 시간이 보장되지 않기 때문에 캐싱이 더 의미있을 수 있도록 무엇이 bfcache를 부적격하게 하는지 이해하고 개발하는 것이 중요합니다.

브라우저가 페이지를 캐시할 수 있도록 하는 모범사례는 다음과 같습니다.

unload 이벤트 사용 금지

unload 이벤트 리스너가 페이지에 존재하기만 해도 브라우저는 페이지가 bfcache에 부적격하다고 판단합니다. unload 이벤트는 bfcache이전에 발생하고, unload를 사용한 것은 페이지가 더 이상 존재하지 않는다는 가정을 하게하기 때문에 문제를 야기할 수 있습니다.

beforeunload는 조건이 있을 때만 사용하기

beforeunload는 크롬과 사파리에는 영향을 받지 않지만, 파이어폭스의 경우 bfcache를 무력화할 수 있으므로 사용해서는 안됩니다.

따라서 아래의 예시와 같이 필요한 경우에 한해 조건부로 이벤트를 추가하고 제거하는 방식으로 사용할 것을 권장합니다.

function beforeUnloadListener(event) {
  event.preventDefault();
  return event.returnValue = 'Are you sure you want to exit?';
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() => {
  window.addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() => {
  window.removeEventListener('beforeunload', beforeUnloadListener);
});

window.opener 참조 제거

window.open() 또는 target="_blank" 속성으로 새 창/탭에서 열린 페이지는 자신의 부모 페이지에 대한 참조(window.opener)를 가지는데, 이것이 null이 아닐 경우 bfcache에 부적격한 것으로 판단합니다.

때문에 rel="noopener"를 사용해 window.opener를 생성하지 않아야, 하는데 이는 Tabnabbing보안 취약점 공격과도 연관된 부분이기도 하므로 추가하는 것이 좋습니다.

열려있는 연결 닫기

페이지를 bfcache에 넣으면 모든 예약된 JS 태스크가 일시중지되고 cache에서 나올 때 다시 시작됩니다. 따라서 PageTransitionEvent 를 통해 작업을 미리 중단/재개하거나 제거/추가하는 편이 좋습니다.

  • 페이지에 끝나지 않은 indexedDB transaction이 있는 경우
  • fetch나 XMLHttpRequest가 진행 중인 경우
  • WebSocket, WebRTC 연결이 살아 있는 경우

만약 페이지가 위의 경우에 해당한다면, pagehide 혹은 freeze에서 이러한 연결을 모두 끊어 버리는 것이 좋습니다. 그리고 pageshow 혹은 resume 이벤트를 통해 페이지가 다시 살아난다면 API를 다시 연결해두면 됩니다.

페이지가 캐싱 가능한지 테스트

chrome://flags

크롬에서 chrome://flags/ 로 접속하여 Back-forward cache 를 활성화하여 테스트해볼 수 있습니다.

또한 Chrome Devtools에서는 페이지를 테스트하여 bfcache에 최적화 되어있는지도 확인해볼 수 있습니다. 이는 Application > Cache > Back-foward Cache에서 가능합니다.

Chrome Devtools

bfcache 비활성화하기

bfcache를 비활성화 할 수 있는방법이 있습니다. 최상위 페이지의 Response header에 Cache-Control에 no-store를 추가하는 것입니다.

Cache-Control: no-store

해당 옵션은 HTTP cache를 위한 옵션이여서 의아할 수는 있지만, bfcache 비활성화가 가능합니다. 하지만 이것은 bfcache뿐만 아니라 다른 캐시도 하지 않도록 유도하여 의도하지 않은 변경이 발생할 수 있습니다.

아직 개발자가 직접 bfcache만을 명시적으로 비활성화를 할 수는 없습니다.

참고자료

'Language > JavaScript' 카테고리의 다른 글

setTimeout 들여다보기  (1) 2023.09.14
자바스크립트 모듈 시스템  (2) 2023.04.16
자바스크립트 반복문  (1) 2022.05.11
JS GC 내부 알고리즘 알아보기  (0) 2021.12.20
const, let 호이스팅 알아보기  (0) 2021.03.17