목차
- bfcache를 직면, bfcache가 뭔가?
- javascript에서 bfcache 다루기
- bfcache 최적화
- bfcache의 문제점 (성능과 분석)
- 참고
웹페이지를 로드하면 server reqeust를 하지 않고 캐싱해 두고 빠르게 띄우는 경우가 있습니다. (특히 뒤로 가기 할 때) 개발자 입장에서 이는 굉장히 난처할 수 있습니다. 웹 캐시는 브라우저 최적화와도 밀접하게 연관되기 때문에 이를 이해하고 활용할 방안이 있는지 정리해 봅니다.
html (js) 프로젝트를 개선 중이었습니다. 서버 스프링 단에 interceptor를 새롭게 만드는 작업을 진행했고 interceptor가 제대로 매핑되었다고 생각한 순간!
브라우저 뒤로 가기로 접근하면 데이터 request를 하지 않고,
캐싱된 데이터를 보여줬습니다. (shit)
이미 브라우저가 캐싱해놓고 있으니 제가 만든 interceptor는 캐치하지 조차 못하겠죠. 애초에 서버 reuqest가 안 들어오는 상황입니다.
bfcache를 직면, bfcache가 뭔가?
bfcache (back / forward cache) 란 사용자가 페이지를 이동할 때 페이지 전체의 스냅샷 (js heap 포함)을 저장하는 메모리 내부 캐시입니다. bfcache가 활성화되면, 네트워크에 전혀 연결하지 않고, 즉각 메모리로부터 해당 웹페이지를 로드 (복원)해 냅니다.
웹사이트 방문 시 특정 페이지에 들어갔다가 곧장 뒤로 가기를 하면 뒷페이지 로딩속도가 굉장히 빠른데 이것이 bfcache 사용 예시라고 볼 수 있습니다. bfcache는 네트워크 연결을 하지 않기 때문에 리소스 다운로드 과정이 생략되어 데이터 사용량을 줄일 수 있습니다.
모두가 사용하는 웹사이트 Youtube에서 쉽게 bfcache를 통한 페이지 복원을 체험할 수 있습니다.
- 유튜브 메인화면의 영상 추천
- 추천 영상 클릭
- 뒤로가기 (영상 추천 알고리즘이 여전히 동일)
HTTP cache와 다른가?
HTTP cache는 대부분 이전에 작성된 요청에 대한 응답 (response)만을 캐싱하는데 반해, bfcache는 메모리에 있는 전체 페이지에 대한 스냅샷입니다. Http cache에 페이지 로딩에 필요한 모든 응답이 들어있는 경우는 드물기 때문에, bcache는 페이지를 메모리로부터 불러와 복원하는데 굉장히 빠르고 유용합니다.
javascript에서 bfcache 다루기
bfcache를 감지할 수 있는 이벤트는 pageshow, pagehide (페이지 전환 이벤트)가 있습니다.
pageshow
pageshow 이벤트는 다음 2가지 경우에 발생합니다.
- 페이지가 처음 로드될 때
- 페이지가 bfcache로부터 복원될 때
pageshow 이벤트의 persisted 속성은 bfcache로부터 복원된 경우 true를 리턴합니다. 따라서 아래와 같이 해당 페이지가 bfcache로부터 복원되었는지 감지할 수 있습니다.
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('This page was restored from the bfcache.');
} else {
console.log('This page was loaded normally.');
}
});
pagehide
pagehide 이벤트는 다음 2가지 경우에 발생합니다.
- 페이지가 정상적으로 언로드 될 때
- 브라우저가 bfcache 하려고 시도할 때
pagehide 에도 peresisted 속성이 있고, 이 속성이 true라면 브라우저가 캐시 하려고 시도한 것입니다. 그러나 이는 시도일 뿐 다른 캐싱 방지 요소에 의해 결과적으로 캐시되지 않을 수도 있습니다.
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.');
}
});
bfcache 해결방안
따라서 저는 캐싱되는 페이지 안에서 아래와 같이 코드를 추가함으로서 해결했습니다.
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// 페이지 새로고침 or 캐싱되면 안되는 데이터 reqeust
...
...
}
});
bfcache 최적화
개발자는 캐시적중률을 최대화해야합니다. 데이터가 언제 캐싱되는지, 언제까지 캐싱되는지 등을 예상해야 합니다. 따라서 페이지를 브라우저로부터 bfcache 하기에 적절한지 부적격한지 잘 판단하게 끔 bfcache를 최적화하는 방법을 알아야 합니다.
- unload 이벤트 사용 안 하기
- window.opener 참조 피하기
- 사용자가 다른 곳으로 이동하려 할 시 기존 연결 닫기
- bfcache로부터 복원 후 오래되거나, 민감한 데이터 업데이트
unload 이벤트 사용 안 하기
unload 이벤트는 bfcache보다 선행됩니다. 브라우저는 페이지에 unload 이벤트 리스너가 추가되어 있는 경우 bfacahce에 적합하지 않은 페이지로 판단하는 경우가 많습니다.
window.opener 참조 피하기
target=blank를 통해 새창을 열도록 작성한 경우, 새로운 창에 window object에 대한 참조가 있습니다. 이때 null 이 아닌 window.opener 참조가 있는 페이지는 안전하게 bfcache에 넣을 수 없습니다. 따라서 rel="noopner"를 사용해서 window.opener참조를 생성하지 않도록 하는 것이 좋습니다.
사용자가 다른 곳으로 이동하려 할 시 기존 연결 닫기
대부분의 브라우저들이 아래 connection이 open 되어있는 경우 해당 페이지를 bfcache에 넣지 않으려고 합니다.
- indexed DB 연결이 open되어있는 페이지
- 진행 중인 fetch(), XMLHttpRequest가 있는 페이지
- WebSocket이 열려있거나 WebRTC 연결이 있는 페이지
bfcache에 넣으면 javascript 작업이 일시 중지되고 페이지가 캐시에서 제거될 때 다시 시작됩니다. 이 작업이 DOM API 액세스, 혹은 다른 도메인 (same-origin X)의 API에 접근하는 것이라면 일시 중지되어도 문제가 없을 겁니다. 그러나 이 작업이 same-origin의 다른 페이지에서 액세스 하는 API인 경우 일시 중지될 경우 문제가 발생할 수 있습니다. 그렇기 때문에 위에 나열한 경우에 대부분의 브라우저가 애초에 해당 페이지를 bfcache 하지 않는 것입니다.
따라서 아래와 같이 pagehide 이벤트를 이용하여 페이지를 떠날 때 기존 연결을 닫아주는 작업이 꼭 필요합니다.
let dbPromise;
function openDB() {
if (!dbPromise) {
dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open('my-db', 1);
req.onupgradeneeded = () => req.result.createObjectStore('keyval');
req.onerror = () => reject(req.error);
req.onsuccess = () => resolve(req.result);
});
}
return dbPromise;
}
// Close the connection to the database when the user is leaving.
window.addEventListener('pagehide', () => {
if (dbPromise) {
dbPromise.then(db => db.close());
dbPromise = null;
}
});
// Open the connection when the page is loaded or restored from bfcache.
window.addEventListener('pageshow', () => openDB());
bfcache로부터 복원 후 오래되거나, 민감한 데이터 업데이트
만일 bfcache로부터 복원된 데이터 중 사용자 상태를 유지해야 하는 민감한 정보 (장바구니, 재고수량 등)가 있을 경우 데이터를 업데이트해야 합니다. 사용자가 로그아웃 한 뒤 다른 사용자가 "뒤로 가기"한다면 로그인 후 페이지를 볼 수 있게 됩니다. 따라서 아래와 같이 bfcache로부터 복원 후 페이지를 갱신할 수 있도록 합니다.
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// 페이지 새로고침 or 캐싱되면 안되는 데이터 reqeust
...
...
}
});
bfcache의 문제점 (성능과 분석)
bfcache는 새 페이지를 로드하지 않고, 기존 페이지를 복원하기 때문에 bfcache가 활성화되는 것 자체가 페이지 데이터 수집 (방문 통계 등)을 과소보고 할 수 있습니다. 더군다나 복원된 페이지 방문은 사용자 성능 측정 시 전체 페이지 방문 속도 중 가장 빠른 페이지 로드일 수도 있습니다. (단순 앞으로 / 뒤로 가기 반복)
따라서 위에서 말한 bfcache 복원을 감지하는 방법을 통해 적절하게 조치할 수 있도록 해야 합니다. 아래는 Google Analytics 수집을 더 정상적으로 수행할 수 있게 하는 코드 예시 입니다.
// Send a pageview when the page is first loaded.
gtag('event', 'page_view');
window.addEventListener('pageshow', (event) => {
if (event.persisted === true) {
// Send another pageview if the page is restored from bfcache.
gtag('event', 'page_view');
}
});
참고
https://codingbeautydev.com/blog/javascript-get-previous-page-url/
https://developer.mozilla.org/en-US/docs/Web/API/PageTransitionEvent/persisted
https://developer.mozilla.org/ko/docs/Web/API/Window/unload_event
'Programming > 삽질일기' 카테고리의 다른 글
[Java] 테스트 코드에서 lombok 사용하기 (0) | 2023.12.29 |
---|---|
[Java] JMH 벤치마크 중 org.openjdk.jmh.runner.RunnerException: ERROR: Another JMH instance might be running. (1) | 2023.12.27 |
[Java] java.lang.UnsupportedOperationException ImmutableCollections (0) | 2023.12.20 |
[TIP] 부정조건문은 의미해석이 쉽지 않다. (0) | 2022.03.08 |
[SPRING] @Qualifier : 동일한 이름의 bean이 존재한다면? (0) | 2022.01.14 |
댓글