웹 페이지에서 컨텐츠를 빠르게 보여주기 위한 트릭


원글
Jake Archibald, https://jakearchibald.com/2016/fun-hacks-faster-content/

몇 주 전에 나는 일 때문에 공항을 이용했는데 여기서 GitHub의 성능에 이상한 점을 발견했다. 링크를 클릭하는 것 보다 새 창으로 접속하는 것이 더 빨랐던 것이다. 아래는 영상으로 그 현상을 캡쳐한 것이다.

https://youtu.be/4zG0AZRZD6Q

영상에서 나는 링크를 클릭하고 바로 주소를 새 탭에 붙여넣었다. 링크를 더 일찍 클릭했지만 새 탭으로 연 페이지의 내용이 더 빨리 보였다.

가진 것을 곧바로 보여주자

페이지 로드를 시작하면 브라우저는 네트워크 스트림 파이프를 HTML파서에 연결한다. 그리고 이 HTML파서는 문서에 파이프라인 형태로 연결된다. 이는 페이지를 전부 다 받을때까지 기다리는 것이 아니라 점진적으로 컨텐츠를 받아온다는 것을 뜻한다. 페이지가 100kb 이어도 유용한 내용은 20kb를 받더라도 렌더링이 가능하다.

이 기능은 예전부터 지원했던 유용한 기능이지만 종종 무시되었다. 대부분의 웹 페이지의 로드에 걸리는 시간에 대한 조언은 "가진 것을 곧바로 보여주자" 이다. 보여주는 것을 미루거나 모든 것을 받아오기 전 까지 기다리지 말자는 것이다.

GitHub은 서버 렌더링에 대한 성능에 신경쓰고 있다. 하지만 같은 탭 내에서의 페이지 이동은 JavaScript를 이용해 다음처럼 구현되어 있다.

// ... 브라우저 네비게이션을 구현하기 위한 많은 코드들
const response = await fetch('page-data.inc');
const html = await response.text();
document.querytSelector('.content').innerHTML = html;
// ... 브라우저 네비게이션을 구현하기 위한 많은 코드들

이 코드는 이 조언을 무시하고 있다. page-data.inc의 다운로드가 모두 끝날때까지 기다리고 있다. 서버 렌더링 버전 (새 탭으로 열었을때) 에서는 이런식으로 데이터를 비축하고 있지 않다. 서버 렌더링 버전은 스트림이고 더 빠르게 컨텐츠를 보여줄 수 있다. GitHub의 클라이언트 렌더러, 즉 여러 자바스크립트들이 이렇게 페이지 로딩을 느리게 만들고 있는 것이다.

나는 단지 GitHub 에 대한 예를 들었지만 이 안티패턴은 거의 모든 단일 페이지 앱에서 발견할 수 있다.

페이지의 컨텐츠를 바꾸는 것은 여러 장점이 있다. 특히 무거운 스크립트나 JavaScript를 재 실행하지 않고 컨텐츠를 변경할때는 말이다. 하지만 스트리밍 기능을 무시할 정도의 장점이라고 생각하지 않는다. JavaScript가 앞서 언급한 스트림 파서에 직접적으로 접근할 방법은 없지만 간접적으로는 방법이 있다.

iframe과 document.write를 이용한 성능 향상법

iframedocument.write()를 사용하기 때문에 JavaScript로 스트림 파서에 간접적으로 접근하는 방법 중 가장 별로인 방법이다. 아래 예제를 보자.

// iframe을 만든다
const iframe = document.createElement('iframe');

// 문서에 숨겨진 형태로 추가한다.
iframe.style.display = 'none';
document.body.appendChild(iframe);

// iframe이 준비될 때 까지 기다린다
iframe.onload = () => {
  // 앞으로의 load 이벤트를 무시한다
  iframe.onload = null;

  // 더미 태그를 만든다
  iframe.contentDocument.write('<streaming-element>');

  // 엘리먼트에 대한 참조를 얻는다
  content streamingElement = iframe.contentDocument.querySelector('streaming-element');

  // 해당 엘리먼트를 iframe문서에서 빼고 부모 문서에 추가한다.
  document.body.appendChild(streamingElement);

  // 컨텐츠를 추가한다 (비동기로 작동한다)
  iframe.contentDocument.write('<p>Hello!</p>');

  // 아래와 같이 컨텐츠를 추가한다. 그러면 끝
  iframe.contentDocument.write('</streaming-element>');
  iframe.contentDocument.close();
};

// iframe을 초기화한다
iframe.src = '';

<p>Hello</p> 엘리먼트가 iframe에 쓰이면 (추가되면) 곧바로 부모 문서에 나타난다! 파서가 새로 만들어져 추가된 엘리먼트들의 스택을 관리하고 있기 때문이다. <streaming-element>를 다른 곳으로 옮겨도 문제가 되지 않는다.

또 이 기술은 innerHTML을 사용하는 것 보다 조금 더 표준 페이지 로딩에 가깝게 동작한다. 특히 파이어폭스를 제외한 나머지 브라우저에서는 스크립트도 다운로드되어 실행된다 (파이어폭스에 버그가 있다) 실은 실행이 안되어야 한다. (Simon Pieters의 지적에 고마움을 표한다) 하지만 Edge, Safari, Chrome은 실행되고 있다.

이 예제에 대한 데모를 만들어 GitHub과 비교했다. 그리고 3g네트웍에서의 성능을 비교했다.

2016-12-12 12 44 38

Raw test data

iframe을 이용한 컨텐츠 스트리밍을 수행한 쪽에서 컨텐츠가 1.5초 더 빨리 나타나고 있다. 아바타 이미지도 훨씬 빨리 나타났다. 스트리밍은 브라우저가 더 빨리 뭔가를 찾을 수 있다는 뜻이므로 더 빨리 병렬로 받을 수 있다는 뜻이다.

위에서 언급한 내용들은 GitHub서버가 주는 HTML에 대해 동작한다. 하지만 프레임웍을 사용한 고유의 DOM 조작을 관리해야 하는 경우 이런 방법으로는 부족할 것이다. 그 경우를 위해 조금 더 좋은 대안을 소개한다.

Newline-delimited JSON

많은 사이트들이 동적 업데이트를 위해 JSON을 사용한다. 하지만 JSON은 스트리밍에 친화적인 포멧이 아니다. 스트리밍 JSON파서가 따로 있긴 하지만 사용하기 까다롭다.

하여 아래처럼 JSON덩어리를 전달하는 대신에

{
  "Comments": [
    {"author":"Alex","body":"..."},
    {"author":"Jake","body":"..."}
  ]
}

개행으로 구분된 JSON을 전달한다.

{"author":"Alex","body":"..."}
{"author":"Jake","body":"..."}

이를 newline-delimited JSON 이라 하고 명백히 표준의 한 종류이다. 파서를 작성하는 것 또한 더 간단하다. 2017년에 우린 조합 가능한 변화되는 여러 스트림을 아래처럼 다룰 수 있다. (fetch API의 파이프라인 지원은 현재 아직 실험적인 기능이긴 하지만 언제 사용가능해질 지 모른다 더 빠를 수도, 더 느릴 수도 있다.)

// Sometime in 2017

const response = await fetch('comments.ndjson');
const comments = response.body
  .pipeThrough(new TextDecoder())
  .pipeThrough(splitStream('\n'))
  .pipeThrough(parseJSON());

for await (const comment of comments) {
  addCommentToPage(comment);
}

splitStreamparseJSON재사용 가능한 변화되는 스트림이다. 그리고 브라우저 호환성을 극대화하기 위해 XHR을 응용할 수 있다.

다시 두 가지 방법을 비교할 수 있는 데모를 만들었다. 아래는 3g망에서의 결과이다.

2016-12-12 12 57 58

Raw test data

일반적인 JSON에 비해 ND-JSON이 1.5 초 빨리 보였다. iframe을 활용한 방법보다 많이 빠르진 않더라도 이런 스트리밍을 사용하지 않으면 거대한 JSON객체가 모두 받아질때까지 엘리먼트를 그릴 수 없다.

너무 서둘러 단일 페이지 앱으로 개발하지 마세요

위에 언급했듯이 GitHub은 웹 페이지 성능을 하락시키는 많은 코드들을 만들었다. 클라이언트에서 브라우저 네비게이팅을 재 구현하는 것은 어렵다. 많은 부분을 잘 바꾸지 않는 이상 그 의미가 없을수도 있다.

만약 우리의 노력을 단순한 브라우저 네비게이션으로 처리한다면

2016-12-12 1 09 47

Raw test data

그냥 자바스크립트를 쓰지 않고 서버 렌더링을 쓰는 편이 훨씬 빠르다. 테스트 페이지는 간단한 댓글만 있는 정도로 간단하다. 페이지가 반복되는 복잡한 컨텐츠가 있을 경우 (예를 들면 끔찍한 광고 스크립트?) 차이가 날 순 있다. 주의를 기울이지 않으면 가성비가 너무 떨어지는 코드를 작성하거나 오히려 더 느리게 만들 수 있다.

HTML파서가 이렇게 동작한다는 것을 알려준 Elliott Sprehn에게 고마움을 표한다.

김민형2016.12.12
Back to list