ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스압/데이터주의] 웹 최적화 방식 모음 - 2. 파싱 및 렌더링 트리
    프로그래밍/Web 2020. 4. 19. 15:54

    2. 파싱 및 렌더링 트리

    이 파트의 핵심은 파싱의 병렬성과 렌더링에 미치는 영향이다.

    크게 배치순서를 바꾸는 것과 제한,  기타 효율적 활용하는 방안으로 나눌 수 있다.

     

    2.1 배치순서

    2.1.1 CSS를 상단에 배치

    개요

    분류: CSS

    HEAD에 스타일 시트를 넣으면 페이지가 점진적으로 렌더링 될 수 있어 좋다.

    CSSOM 트리는 CSS를 모두 해석해야 구성되고, 구성되지 않으면 렌더링이 차단되기 때문이다.

    반면 DOM 트리는 순차적으로 구성될 수 있어 점진적 렌더링이 가능하다.

     

    성능에 관심이 있는 프론트엔드 엔지니어들은 점진적으로 페이지를 읽어오길 원한다.

    즉, 브라우저에 가능하면 빨리 어떠한 컨텐츠 표시할 수 있기를 원한다.

    이것은 컨텐츠가 많은 페이지와 느린 인터넷 접속 사용자에게 특히 중요한데, 사용자에게 진행률 표시기(progress indicator)와 같은 시각적 피드백 제공의 중요성은 잘 연구되고 문서화 되어있다.

    브라우저가 페이지를 점진적으로 읽어올 때, 상단의 헤더(header), 메뉴바(navigation bar), 로고 등은 페이지를 기다리는 사용자에게 시각적 피드백을 제공하는 역할을 하고 이것은 전반적인 사용자경험(UX)을 향상시킨다.

     

    반면, 문서하단에 CSS를 배치한다면 많은 브라우저에서 점진적으로 렌더링되는 것에 악영향을 끼친다.

    스타일 시트가 변경될 때 페이지 구성요소를 다시 그리는 것을 피하기 위해 렌더링을 차단하여 빈페이지를 보게된다는 것.

    빈 흰색 화면이나 스타일이 지정되지 않은 콘텐츠의 깜박임 중 어느 것도 위험을 감수할만한 가치가 없다.

     

    다시 말하지만, 최적의 솔루션은 HTML 사양을 따르고 스타일 시트를 문서 HEAD에 로드하는 것이다.

    HTML 스펙은 명확하게 스타일이 페이지의 HEAD에 포함시킬 것을 기술한다

    Unlike A, [LINK] may only appear in the HEAD section of a document, although it may appear any number of times.
    a태그가 여러 번 나타날 수 있는 것과 달리, [LINK]는 오직 문서의 HEAD 섹션에 표시 될 수 있습니다.

     

    해결방법

    아래와 같이 배치

    <head>
      ...
      <link href="STYLE.css" rel="stylesheet" />
      ...
    </head>

     

    2.1.2 Script를 하단에 배치

    개요

    분류: Javascript

    스크립트로 인해 발생하는 첫번째 문제는 병렬 다운로드를 차단한다.(스크립트의 순서를 지키기 위해)

    HTTP / 1.1 규격은 각 브라우저 호스트가 두 개의 구성요소 이상(병렬로) 다운로드 할 것을 제안한다.

    여러 호스트 이름에서 이미지를 제공하면 동시에 두 번 이상 다운로드 할 수 있지만, 스크립트를 다운로드하는 동안 브라우저는 다른 호스트 이름에서도 다른 다운로드를 시작하지 않는다.

     

    하지만 무엇보다 중요한 것은 자바스크립트는 DOM과 CSSOM 트리를  변경할 수  있기 때문에 파싱을 차단하는 리소스라는 것이다.

    외부에서 가져오는 스크립트도 모두 다운로드되고 실행될 때까지 DOM 트리 생성를 중단한다.

    • head에 위치시: 스크립트를 실행한 후 body를 렌더링
    • body에 위치시: DOM 랜더링 도중 스크립트를 만나면 처리완료시까지 랜더링이 중단

     

    따라서 /body의 직전에 두는 것이 가장 현명한 방법.

    자바스크립트와 렌더링은 자바스크립로 상호작용 추가라는 글을 읽어보면 좋다.

     

    해결방법

    아래와 같이 배치

    <body>
      ...
      <script src="SCRIPT.js" type="text/javascript"></script>
    </body>

     

    하지만, 어떤 상황에서는 스크립트를 맨 아래로 옮기기가 쉽지 않다.

    예를 들어 스크립트가 페이지 내용의 일부를 삽입하기 위해 document.write를 사용하는 경우 페이지에서 하단 이동할 수 없으며 Scope 문제가 있을 수도 있다.

    대부분의 경우 이러한 상황을 해결하는 방법이 있다.

     

    바로 비동기(async)나 지연된(deferred) 스크립트를 사용하는 것이다.[<script> 요소]

    스크립트에 document.write가 포함되어 있지 않으며(지정된 순서대로 실행된다는 보장이 없기 때문) 브라우저에서 렌더링을 계속할 수 있는 단서다. [async vs defer attributes]

     

     

    둘다 다운로드 받는 동시에 렌더링을 할 수 있지만 다음과 같은 특성이 있다.

    • async: 다운이 완료되면 실행
    • defer: HTML 파싱이 끝나면 실행 

     

    2.1.3 우선순위에 대한 힌트 제공

    개요

    분류: Content

    우선순위에 대한 힌트를 브라우저에 제공하여 로드를 할 수 있다.

    바로 전에 언급했던 async, defer와 같은 예도 속한다.

     

    일단 preload prefetch에 대해서만 다루어보도록 하자.

    자세한 것은 MDN의 link항목와 참조 링크들을 참조. [Preload, Prefetch And Priorities in Chrome, 리소스 우선순위 - preload, preconnect, prefetch, How to improve web performance By using(Preload, Preconnect, Prefetch)Link prefetching FAQ]

     

     

    - Preload

    브라우저에게 페이지에서 필요한 자원을 일찍이 fetch하라는 속성이다.
    현재 페이지에서 빠르게 가져와야 하는 리소스에 사용되는 속성이며 반드시 해야하는 작업이라고 알려준다.

     

    - Prefetch

    미래에 필요할 수 있는 리소스를 가져와야 할 때 사용되는 속성이다.

    현재 페이지 로딩이 마치고 사용 가능한 대역폭(bandwidth)이 있을 때(다운 받을 여유가 생겼을 때) 가장 낮은 우선순위로 리소스를 가져온다는 뜻.

     

     

    이번에는 dns-prefetch와 preconnect 차례.

    이 둘은 현재 페이지에서 외부 도메인의 리소스를 참고하는 것을 브라우저에게 알려 미리 외부 도메인과 연결을 설정할 수 있게 하는 것이 목표.

     

    - DNS-Prefetch

    다른 출처의 DNS 조회를 처리해 놓는다.

     

    - Preconnect

    DNS 조회 뿐만 아니라 TCP 핸드 쉐이킹까지 해결한다.

    preconnect를 사용하면 브라우저가 사이트에 필요한 연결을 미리 예상 할 수 있게 된다.

    따라서 브라우저는 필요한 소켓을 미리 설정할 수 있기 때문에 DNS, TCP, TLS 왕복에 필요한 시간을 절약 할 수 있게 된다.

     

    해결방법

    - Preload

    <link rel="preload" as="script" href="super-important.js">
    <link rel="preload" as="style" href="critical.css">

    as 속성을 사용하여 리소스의 유형을 알려줘야 한다.

    브라우저는 올바른 유형이 설정되어 있지 않으면 미리 가져온 리소스를 사용하지 않는다.

    리소스를 두 번 가져오게 하거나, 필요하지 않는 것을 가져오지 하지 않도록 주의해야 한다.

     

    웹폰트에 많이 사용한다.

    브라우저에서 글꼴을 요청이 바로 일어나는 것이 아니라 CSSOM이 생성되고 렌더링 트리를 생성할 때서야 비로소 요청이 발생하기 때문이다.

     

    <link rel="preload" as="font" crossorigin="crossorigin" type="font/woff2" href="myfont.woff2">

    단, 폰트는 다른 호스트에 있는 경우가 대부분이므로 crossorigin을 표시해주는 것이 좋다. 

     

    - Prefetch

    받아온 요청이나 자원이 유저의 앞으로도 navigation(예를 들어 다른 뷰나 페이지 사이) 지속되어야 한다면 쓸 수 있다.

    만약 Page 1이 Page 2에 꼭 필요한 자원을 prefetch하는 요청을 날리는 경우라면, 중요한 자원과 navigation requests는 병행으로 수행될 수 있다.

     

    만약에 이 경우에 preload를 사용한다면 즉시 Page A의 unload가 취소될 것이다.

    <link rel="prefetch" href="page-2.html">

    역시 재정의하여 사용할 수 없다는 점은 명심해야 한다.(두번 로드됨)

     

    - importance(실험적)

    preload와 prefetch시에만 쓸 수 있는 속성이다.

    • auto: 브라우저의 휴리스틱에 의해 우선순위가 자동적으로 결정된다.
    • how: 높은 우선순위라는 것을 알려줌
    • low: 낮은 우선순위라는 것을 알려줌

     

    - DNS-Prefetch

    <link  rel="dns-prefetch"  href = "//example.com">

     

    DNS Prefetch나 Preconnect는 정확한 경로를 알 수 없을 때나 미디어 스트리밍시 유용하다.

     

    정확한 경로를 알 수 없을 때

    주어진 CDN으로 부터 리소스를 가져와야 한다는 것은 알지만 정확한 경로를 모르는 상황이 발생 할 수 있다.

    예를 들면 브라우저 별로 가져와야 하는 라이브러리 등의 버전이 다를 때, CDN 주소는 알지만 정확한 경로는 알지 못하는 상황을 들 수 있다.

    브라우저는 파일이 필요하기 전에는 리소스를 가져오지 않지만 적어도 연결은 먼저 처리해서 리소스를 요청하고 가져오는 여러번의 왕복을 가다리지 않아도 된다.

     

    미디어 스트리밍

    스크립트가 로드되고 스트리밍 데이터를 처리할 준비가 될 때 까지 스트리밍을 기다리고 싶을 수 있다. 

    미리 연결을 하기 때문에 리소스를 가져올 준비가 되면 연결을 설정하는 것이 아니라 미리 연결된 설정에 따라 리소스를 가져와 연결을 설정하는 대기 시간을 줄 일 수 있다.

     

    - Preconnect

    <link rel="preconnect" href="https://example.com">

     

    1. href 속성에 의해 URL을 해석, URL이 유효한 URL인지 아닌지 해석하여 아닐경우 error처리, HTTP/HTTPS인지 판단
    2. 유효할 경우 이 url을 origin으로 판단
    3. cors에 대한 상태를 대상 엘리먼트의 crossOrigin 속성에 할당
    4. cors의 속성의 값이 anonymous 이거나 credentials이 false가 아닐경우 연결을 시도
    5. http의 경우 (DNS+TCP), https의 경우 (DNS+TCP+TLS)를 수행, 이러한 커넥션을 열어두게 되며, 얼마나 많은 연결을 하게 될지는 user agent가 결정

     

    구글 폰트를 다운받을 때 글꼴을 바로 다운받기 위해 쓰는 트릭이기도 했다. [Make your Google Fonts render faster]

     

    주의사항

    Preconnect는 외부 도메인과 연결을 구축하기 때문에 많은 CPU 시간을 차지할 수 있으며 보안 연결의 경우 더 많은 시간을 차지 할 수 있다.

    Preconnect는 10초 이내에 해당 서버에게 요청할 자원이 없으면 끊어진다는 것을 명심하자.

     

    2.1.4 스크롤 없이 볼 수 있는 영역 우선

    개요

    분류: content

    스크롤 없이 볼 수 있는 영역(ATF, Above The Fold)을 먼저 로드하도록 구성해야 한다. [Authoring Critical Above-the-Fold CSS]

    스크롤 없이 보이는 부분에 우선순위 부여[from stackoverflow]

     

    해결방법

    - 레이아웃 구성

    시각적으로는 동일하지만 서로 다른 레이아웃으로 나누어 만든다면 가능하다. [Prioritize visible content]

    이렇게 만들면 ATF의 CSS를 위한 부분과 나머지로 분할할 수도 있다.

     

    - 주요 컨텐츠 우선

    또한 페이지의 주요 컨텐츠를 먼저 로드하는 것이 좋다.

    서버의 초기 응답에서 페이지의 중요한 부분을 렌더링하는 데 필요한 데이터를 즉시 보내고 나머지를 지연하도록 페이지를 구성해야한다는 것이다.

     

    사이드바, footer영역, 기타 제3자 위젯(SNS 공유 위젯 등)등은 메인 컨텐츠보다 중요치 않다.

    따라서 컨텐츠가 점진적으로 렌더링 되도록 만든다.

    <html>
    <body>
      <content>
      </content>
      <sidebar>
      </sidebar>
    </body>
    </html>

     

    - CSS 분리 및 비동기 로드

    중요한 CSS 결정 도구를 사용하면 첫화면에 사용되는 CSS만 따로 뽑아 인라인으로 사용할 수 있다. [Extract critical CSS, Critical-path (Above-the-fold) CSS Tools, How to speed up the initial rendering of a web page]

     

    가장 간단히 비동기적으로 로드할 수 있는 방법은 다음처럼 설정하는 것이다. [The Simplest Way to Load CSS Asynchronously, The Simplest Way to Load CSS Asynchronously, Loading CSS without blocking render]

    <link rel="stylesheet" href="/path/to/my.css" media="none" onload="if(media!='all')media='all'">

    media를 none으로 설정하면 화면의 렌더링을 막지 않기 때문에 일단 다운로드 후, onload 이벤트가 일어날때까지 미룬후 all로 바꾸어 로딩하자는 일종의 꼼수.

     

    라이브러리 중에서 loadCSS를 사용하면 나머지 CSS를 비동기적으로 로드할 수 있다.[Authoring Critical Above-the-Fold CSS]

    또는 다음처럼 하여 첫 번째 페인트 후 로드되도록 하는 것이 가능. (역시 렌더링을 차단하지 않는다) [차이: Google deferred CSS vs loadCSS]

    <html>
      <head>
        <style>
          .blue{color:blue;}
        </style>
        </head>
      <body>
        <div class="blue">
          Hello, world!
        </div>
        <noscript id="deferred-styles">
          <link rel="stylesheet" type="text/css" href="small.css"/>
        </noscript>
        <script>
          var loadDeferredStyles = function() {
            var addStylesNode = document.getElementById("deferred-styles");
            var replacement = document.createElement("div");
            replacement.innerHTML = addStylesNode.textContent;
            document.body.appendChild(replacement)
            addStylesNode.parentElement.removeChild(addStylesNode);
          };
          var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
              window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
          if (raf) raf(function() { window.setTimeout(loadDeferredStyles, 0); });
          else window.addEventListener('load', loadDeferredStyles);
        </script>
      </body>
    </html>
    

     

    유용한 웹팩 플러그인을 찾았다.

     

    나중에는 웹컴포넌트의 HTML Import를 사용할 수 있다면, 렌더링을 차단하지 않고도 CSS를 로드할 수도?? [HTML Rocks: HTML Import]

    HTML Import가 표준으로 안들어 갈 것 같은 분위기가 문제일것 같긴 하다만.

     

    2.1.5 사이트 빌더

    개요

    분류: Conent

    자바스크립트로 클라이언트 렌더링을 하는 것보다 HTML과 CSS를 생성 시킨채 배포한다면 동적인 작업이 줄어들고 봇이 해석하기 편하므로 성능/SEO에 모두 유리하다. [Is 0kb of JavaScript in your Future?]

     

    해결방법

    사이트 빌더들을 사용해보자.

     

    • Astro: React, Preact, Vue, Svelte, Solid등 가능, Partical hydration 지원
    • Elder.js: Svelte용, Partical hydration 지원, 사용자 지정 라우팅
    • Gatsby: React용, 정적 사이트 생성(SSG, Static Site Generation), 지연된 정적생성(DSG, Deferred Static Generation), 서버 사이드 렌더링(SSR, Server Side Rendering) 처럼 다양한 렌더링 옵션 제공
    • Eleventy: 정적 사이트 생성기로, 마크다운, 11ty.js, Liquid(또 다른 문서), Nunjucks, Handlebars, EJS, Pug와 같은 HTML 템플릿 지원. JS 사용을 최대한 피하도록 구성

    기타 차이점은 Astro vs X에서 잘 정리해둔편.

     

    react-snap같은 프리렌더러, react static같이 SSG에 최적화된 프레임워크도 살펴볼만하다.

     

    2.1.6 Lazy load

    개요

    분류: Conent, Image

    느긋하게(Lazy) 로딩을 하면 시간을 아낄 수 있다. [이미지 및 동영상의 지연 로딩, Lazy loading, Lazy Loading Images – The Complete Guide, Image Lazy Loading 기법 그리고 Google I/O 에서의 새로운 방법, Lazy Loading 이란?]

     

    하지만 주의해야 할 것이 존재한다.

    • 처음 보이는 화면은 하지 않는 것이 나음
    • 스크롤을 내려야 이미지 로드를 시작해 느려보일수도 있음
    • 자리 표시자가 없으면 레이아웃 변경이 생길 수 있음
    • JS로 대형 이미지를 로딩시 잠시 반응하지 않을 수 있음
    • 로드가 실패할 수 있음
    • JS를 사용하지 못할 수도 있음

     

     

    해결방법

    - Intersection Observer

    IntersectionObserver를 사용하면 간단하게 lazy 로드를 할 수 있다.

     

    <img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="저는 이미지입니다!">
    • lazy란 클래스 속성으로 선택할 요소 지정
    • src에는 자리 표시자 이미지
    • data-src와 data-srcset으로 로드할 이미지의 URL을 담음

     

    이때 Intersection Observer를 사용하면

    document.addEventListener("DOMContentLoaded", function() {
      var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
    
      if ("IntersectionObserver" in window) {
        let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
          entries.forEach(function(entry) {
            if (entry.isIntersecting) {
              let lazyImage = entry.target;
              lazyImage.src = lazyImage.dataset.src;
              lazyImage.srcset = lazyImage.dataset.srcset;
              lazyImage.classList.remove("lazy");
              lazyImageObserver.unobserve(lazyImage);
            }
          });
        });
    
        lazyImages.forEach(function(lazyImage) {
          lazyImageObserver.observe(lazyImage);
        });
      } else {
        // Possibly fall back to a more compatible method here
      }
    });
    

    DOMContentLoaded 시, lazy 클래스의 모든 이미지에 대한 DOM을 조회해 lazy load 대상 이미지 영역에 진입시 콜백을 실행하는 옵저버를 생성한다.

     

    - 이벤트 핸들러 사용

    scroll 이벤트, resize 이벤트를 받아서 getBoundingClientRect()같은 DOM API를 사용해도 뷰포트 위치를 계산할 수 있다.

    (호환성이 좋다.)

     

    아래 코드를 사용하면 위와 같은 방식으로 로드할 수 있다.

    document.addEventListener("DOMContentLoaded", function() {
      let lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));
      let active = false;
    
      const lazyLoad = function() {
        if (active === false) {
          active = true;
    
          setTimeout(function() {
            lazyImages.forEach(function(lazyImage) {
              if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") {
                lazyImage.src = lazyImage.dataset.src;
                lazyImage.srcset = lazyImage.dataset.srcset;
                lazyImage.classList.remove("lazy");
    
                lazyImages = lazyImages.filter(function(image) {
                  return image !== lazyImage;
                });
    
                if (lazyImages.length === 0) {
                  document.removeEventListener("scroll", lazyLoad);
                  window.removeEventListener("resize", lazyLoad);
                  window.removeEventListener("orientationchange", lazyLoad);
                }
              }
            });
    
            active = false;
          }, 200);
        }
      };
    
      document.addEventListener("scroll", lazyLoad);
      window.addEventListener("resize", lazyLoad);
      window.addEventListener("orientationchange", lazyLoad);
    });

     

    - Loading 속성

    <img> <iframe>같은 경우 loading 속성을 이용해 사용이 가능하다.

    <img src="image.jpg" loading="lazy" alt="..." />
    <iframe src="video-player.html" loading="lazy"></iframe>

     

    - 비디오 Lazy Load

    이미지처럼 동영상도 Lazy Load 할 수 있다.

    일반적으로 <video>로 사용하지만, 제한적으로는 <img> 를 사용하는 것도 쓸만한 모양

     

    자동 재생하지 않는 경우

    자동 재생(Autoplay)을 하지 않는 경우 preload를 할 필요가 없다.

    <video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
      <source src="one-does-not-simply.webm" type="video/webm">
      <source src="one-does-not-simply.mp4" type="video/mp4">
    </video>

     

    자동 재생을 해야할 때 프리로드하지 않으면, 느려질 수도 있기 때문에 사용하지 않는 것이 좋다. [Fast Playback with Video Preload]

     

    GIF

    GIF는 자동 재생이 되어야 한다는 것이 가장 큰 차이다.

    <video autoplay muted loop playsinline class="lazy" width="610" height="254" poster="one-does-not-simply.jpg">
      <source data-src="one-does-not-simply.webm" type="video/webm">
      <source data-src="one-does-not-simply.mp4" type="video/mp4">
    </video>

     

    역시 data-src를 src로 바꿔주면 끝.

    document.addEventListener("DOMContentLoaded", function() {
      var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));
    
      if ("IntersectionObserver" in window) {
        var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
          entries.forEach(function(video) {
            if (video.isIntersecting) {
              for (var source in video.target.children) {
                var videoSource = video.target.children[source];
                if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
                  videoSource.src = videoSource.dataset.src;
                }
              }
    
              video.target.load();
              video.target.classList.remove("lazy");
              lazyVideoObserver.unobserve(video.target);
            }
          });
        });
    
        lazyVideos.forEach(function(lazyVideo) {
          lazyVideoObserver.observe(lazyVideo);
        });
      }
    });
    

     

    - 주의사항

    처음 보이는 화면

    메뉴나 헤더등에는 적용하지 말고, 메인 컨텐츠에서도 처음나오는 3~4개까지는 적용을 하지 않으면 보완할 수 있다.

     

    스크롤을 내려야 로드 시작

    마진을 주면 스크롤 전에 로드를 시작하여 로드가 느려보이는 점을 보완할 수 있다.

     

    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      // Lazy loading image code goes here
    }, {
      rootMargin: "0px 0px 256px 0px"
    });

    처럼.

     

    자리 표시자

    설명할 수 있는 옵션이 많아 렌더링의 UX 파트에서 따로 다루도록 한다.

     

    JS로 대형 이미지 로드

    곧 나올 Streams를 이용해 청크 단위로 다운을 받거나 Image()를 이용한다. [Image loading with image.decode()]

    const newImage = new Image();
    newImage.src = "my-awesome-image.jpg";
    
    if ("decode" in newImage) {
      // Fancy decoding logic
      newImage.decode().then(function() {
        imageContainer.appendChild(newImage);
      });
    } else {
      // Regular image load
      imageContainer.appendChild(newImage);
    }

    단, Lazy 로드 알고리즘을 복잡하게 만들수도 있다.

     

    로드 실패시

    다음처럼 예외처리를 하고, 다시로드 버튼이나 오류 메세지를 표시할 수 있다.

    const newImage = new Image();
    newImage.src = "my-awesome-image.jpg";
    
    newImage.onerror = function(){
      // Decide what to do on error
    };
    newImage.onload = function(){
      // Load the image
    };

     

    JS 미사용

    <noscript>로 JS를 사용못할 때를 대비할 수 있다.

    <!-- An image that eventually gets lazy loaded by JavaScript -->
    <img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load.jpg" alt="저는 이미지입니다!">
    <!-- An image that is shown if JavaScript is turned off -->
    <noscript>
      <img src="image-to-lazy-load.jpg" alt="저는 이미지입니다!">
    </noscript>

     

    자세한 것은 자바스크립트 가용성을 참고.

     

    - 라이브러리

    • lazysizes: 이미지와 iframe을 lazy load하는 라이브러리로 다양한 기능을 가지고 있다. 여러가지 확장이 있다.
    • lozad.js: Intersection Observer를 이용하는 경량 라이브러리
    • yall.js: Intersection Observer를 이용하는 경량 라이브러리2
    • blazy: IE7+를 지원하는 가벼운 라이브러리
    • react-lazyload: 리엑트 개발자에게 익숙한 방식을 제공

     

    +

    기타 이미지 로딩 최적화는 The Humble <img> Element And Core Web Vitals에서 잘 설명하고 있다. (트윗)

     

    2.1.7 버퍼를 일찍 비우기(Flush)

    개요

    분류: Server

    사용자가 페이지를 요청하면 백엔드 서버가 HTML 페이지를 함께 연결하는 데 200 ~ 500ms가 소요될 수 있다.

    이 시간 동안 데이터가 도착할 때까지 브라우저는 유휴 상태이다.

     

    이를 보완할 만한 대책은 flush() 함수(PHP 기준)다.

    부분적으로 준비된 HTML 응답을 브라우저로 보내는것을 허용함으로써 백엔드가 나머지 HTML 페이지로 바쁠때 브라우저는 구성요소를 가져오는 것(fetching)을 시작할 수 있다.

    이 이점은 주로 바쁜 백엔드 또는 가벼운 프론트엔드에서 볼 수 있다.

     

    Flushing

    head에 대한 HTML은 일반적으로 생성하기 쉬우며 백엔드가 동작하는 동안 브라우저가 CSS나 JavaScript 파일들을 병렬적으로 가져오도록 포함 할 수 있다.

    따라서 </head> 바로 다음이 플러시를 고려하기 좋은 위치이다.

     

    해결방안

    PHP, ASP, JSP와 같은 템플릿 엔진들은 다음처럼 플러싱을 할 수 있다. [HTTP Archive: adding flush, Progressive rendering via multiple flushesSpeeding Up Page Load with Early Flush, Async Fragments: Rediscovering Progressive HTML Rendering with Marko]

     ... <!-- css, js -->
    </head>
    <?php flush(); ?>         <!-- PHP -->
    <% Response.Flush(); %>   <!-- ASP.NET -->
    <% out.flush() %>         <!-- JSP -->
    <body>
    ... <!-- content -->

     

    파이썬 Flask의 경우 스택오버플로우를 참고해볼 수 있다.

     

    Marko라는 템플릿 엔진은 플러싱의 이점을 극대화 하여 비동기 프레그먼트를 비순차적으로 플러싱 할 수 있다.

    Out-of-order flushing of Async Fragments

    Async Fragment는 리엑트의 React.lazy와 비슷하다.

     

    페이스북의 빅파이프(BigPipe)로 유명하다. [BigPipe: Pipelining web pages for high performance(번역), Faster Web Page Loading with Facebook BigPipe, Facebook Bigpipe Technique Algorithm]

    여기 몇가지 괜찮은 구현체/샘플이 있다.

    웹소켓을 이용해 파이프라이닝을 해보려는 시도도 있다. [SPADOCK: Adaptive Pipeline Technology for Web System using WebSocket]

     

    기타 흥미로울 만한 것은 처음에만 정적 페이지, 이후에는 동적으로 로드하는 유튜브의 SPF(Structured Page Fragments)와 세분화된 Lazy loading을 제공하는 Qwik 정도?

     

    2.2 제한

    2.2.1 유효성

    개요

    분류: Content, CSS

    유효성(Validation)을 지키지 않으면 파싱시 느려질 수도 있다.

    다음 성능과 관계된 몇가지 예시이다.

     

    - 닫힌 태그

    HTML의 frame, img, li, p등을 닫힌태그(</태그>)  없이 암시적으로 사용해도 페이지는 잘 렌더링 된다.[Building High Performance HTML Pages]

     

    <p>The following is a list of ingredients.
    <ul>
    <li>flour
    <li>sugar
    <li>butter
    </ul>

    실제로 렌더링 가능

     

    하지만 끝나는 위치를 미리 결정할 필요 없기 때문에 닫혀있는 상태 파싱이 더 빠르다.

     

    - 크기 지정

    이미지의 width와 height 속성을 생략시 크기를 정확히 모르기 때문에 Placeholder를 할당하고, 리플로우시 레이아웃이 업데이트 되어야 하기 때문에 성능에 영향을 미친다.

     

    테이블도 역시 width와 height 속성을 생략하면 리플로시 업데이트가 필요하다.

    또한

      table-layout: fixed;

    를 사용하고, <col><colgroup>에 width를 지정하는 것이 좋다.

     

    - 빈문자열 src 이미지

    빈 문자열 src 속성을 가진 이미지가 하나 이상 발생할 것으로 예상되고 두 가지 형태로 나타난다.

    1. straight HTML

    <img src="">

    2. JavaScript

    var img = new Image();
    img.src = "";

     

    왜 이런 동작은 좋지 않을까? 

    1. 특히 하루에 수백만 페이지를 보는 페이지에 대해 대량의 예기치 않은 트래픽을 보내 서버를 손상시킨다.
    2. 서버는 절대 보여지지 않을 페이지를 생성하는 사이클(cycles)을 컴퓨팅 하는 것으로 낭비된다.
    3. 사용자 데이터는 오류가 생길 가능성이 있다.
      만약 요청이 추적상태에 있다면, 쿠키 또는 다른 방식으로 데이터를 파괴할 가능성이 있다.
      비록 이미지 요청이 이미지를 반환하지 않더라도, 모든 쿠키들을 포함하는 브라우저에 의해 모든 header들은 읽고 수락될 수 있다. 응답의 나머지가 버려지는 동안, 이미 손상 되었을지도 모른다.

     

    이 작동의 근본 원인은 URI 해상도가 browser에서 실행되는 방식 때문이다.

    이것은 RFC 3986- Uniform Resource Identifiers에 정의되어 있다.

    빈 문자열이 URI로 발생되면, 그것은 상대적인 URI로 간주되며,  5.2 섹션에 정의된 알고리즘에 따라 해결된다.

    구체적인 예를 들자면, 5.4 섹션에 등록된 빈 문자열이다.

    Internet Explorer가 부정확하게 실행되는 동안, 명백히 RFC 2396 - Uniform Resource Identifiers 사양(이것은 RFC 3986에 의해 더 이상 사용되지 않음)의 초기버전과 일맥상통하는 Firefox, Safari, 그리고 Chrome 섹션은 모두 사양마다 정확하게 빈 문자열을 해결할 것이다.

    그래서 기술적으로, browsers는 상대적 URIs를 해결하기 위한 것들을 수행 중이다.

    이 문맥에서의 문제는, 빈 문자열은 명확히 의도적이지 않다는 것이다.

     

    HTML 5는 섹션 4.8.2에서 추가요청을 하지 않는 browsers를 지시하기 위해 태그의 src 속성의 설명을 추가한다:
    src 속성은 반드시 존재해야 하고, 페이지 및 script되지 않으며 쌍방향이 아니고(non-interactive), 선택적으로 애니메이션(optionally animated)된 이미지 자원(image resource)을 참조하는 유효한 URL을 반드시 포함해야 한다. 만약 요소(element)의 기본 URI가 문서의 주소와 동일한 경우, src 속성값은 빈 문자열이어서는 안 된다.

     

    다행히 앞으로 브라우저에는 이 문제가 없을 것이다.

    하지만 불행히도 <script src=""> 및 <link herf="">에는 해당 조항이 없으니 브라우저가 실수로 이 동작을 구현하지 않도록 조정해야 할 때가 있다.

    이 규칙은 Yahoo!의 JavaScript 전문가 인 Nicolas C. Zakas에서 영감을 받았으며 자세한 내용은 "Empty image src can destroy your site "라는 기사를 확인하면 된다.

     

    - 기타

    기타 파싱시 문제가 생기면 오류 수정과정이 필요하니 유효성이 지켜지는 것이 좋다.

    다음은 파서의 에러핸들링과 처리 방식이다.

    An introduction to error handling and strange cases in the parser

     

    측정방법

     

    src="" 를 가진 img 갯수는 중복 스크립트를 찾을 때의 코드를 조금만 수정하면 된다.

    const imgs = document.getElementsByTagName("img");
    [...imgs].map(img => img.src)
      .filter(src => src === "")

     

    2.2.2 @import보다 <link> 사용

    개요

    분류: CSS
    이전 모범 사례 중 하나는 점진적 렌더링을 허용하기 위해 CSS가 최상위에 있어야 한다는 것이 있었다.

     

    @import를 사용하면 CSS를 병렬로 다운로드할 수 없으며,

    IE에서는 페이지 하단에서 @import사용하는 것과 동일하게 작동 하므로 사용하지 않는 것이 가장 좋다.

     

    해결방안

    이러한 @import보다

    /* at CSS file */
    @import url("EXAMPLE.css")

     

    <link>를 사용한다.

    <!-- at HTML file -->
    <link href="EXAMPLE.css" rel="stylesheet" />

     

    2.2.3 미디어 쿼리 사용하기

    개요

    분류: CSS

    특정 조건에서만 필요한 CSS가 있을 때 미디어 쿼리를 사용하면 불필요한 블로킹을 막을 수 있다.

    즉, 해당 스타일이 참일 때만 적용된다는 것.

     

    미디어 쿼리 구분은 미디어 유형(media-type), 미디어 특성(media-feature-rule), 논리연산자로 이루어져 있다.

     

    미디어 유형

    • all: 모든, 기본값
    • print: 인쇄 결과물이나 미리보기시 사용
    • screen: 화면
    • speech: 음성 합성장치

     

    주요 미디어 특성

    • width/height: 뷰포트 너비/높이
    • aspect-ratio: 뷰포트 가로세로비 
    • orientation: 뷰포트 방향
    • resolution: 출력장치의 해상도
    • scan: 출력장치의 스캔 방법(초당 60회, 30회 등)
    • color: 출력장치의 색상 채널별 비트 수, 흑백은 0

    이외에 고대비, 다크모드등 선호도를 조절할 수 있으므로, 잘 써보도록 하자.

     

    논리연산자

    • ,: 논리합, 흔히 생각하는 or과 같다.
    • and: 논리곱
    • not: 부정, 개별의 쿼리가 아니라 전체에만 적용된다.
    • only: 미디어 쿼리를 미지원하는 낡은 브라우저에 대응하여 특정 스타일 적용

     

    해결방법

    미디어 쿼리를 사용하는 방법은 CSS 내부와 외부 2가지 방법으로 나눌 수 있다.

     

    내부

    /* 형식 */
    @media 미디어타입 논리연산자 (미디어특성) { ... }
    
    /* 샘플 */
    @meida all { ... }
    @media print { ... }
    @media (orientation: portrait) { ... }
    @media (min-width: 30em) and (max-width: 50em) { ... }

     

    외부

    <!-- 형식 -->
    <link rel="stylesheet" type="text/css" href="EXAMPLE.css" media="미디어타입 논리연산자 (미디어특성)"/>
    
    <!-- 샘플 -->
    <link rel="stylesheet" type="text/css" href="style.css"/>
    <link rel="stylesheet" type="text/css" href="print.css" media="print" />
    <link rel="stylesheet" type="text/css" href="portrait.css" media="orientation:portrait" />
    <link rel="stylesheet" type="text/css" href="min_max.css" media="(min-width: 30em) and (max-width: 50em)" />

     

    이중 어떤 것을 사용하면 좋을까?

    만약 외부분기를 사용할 때 조건에 따라 다운로드한다면 상당히 유용할 것이다.

    그러나 불행히도 <link>의 미디어 쿼리의 조건이 거짓일지라도 CSS는 다운된다.

    따라서 CSS 파일은 병합하고, CSS 코드 내부에서 분기하는 것이 좋다.

     

    2.2.4 리다이렉션 방지

    개요

    분류: content

    리디렉션(재전송)은 301이나 302 상태 코드를 사용하여 수행한다. 

    다음은 301 응답의 HTTP 헤더 예이다.

          HTTP/1.1 301 Moved Permanently
          Location: https://example.com/newuri
          Content-Type: text/html

     

    브라우저가 자동으로 사용자를 Location 필드에 지정된 URL로 이동시킨다 .

    리디렉션에 필요한 모든 정보는 헤더에 있으며, 응답한 본문은 일반적으로 비어 있다.

    Expires또는 Cache-Control과 같은 추가 헤더를 제외하고 301도 302 응답 어느 쪽도 실제로 캐시되지 않는다.

     

    다음의 메타 리프레시 태그나 자바스크립트는 사용자를 리다이렉션 할 수 있지만..

    <meta http-equiv="Refresh" content="0; URL=https://example.com/newuri/">
    window.location = "https://example.com/newuri/";

     

    반드시 리다이렉션을 해야 한다면,  뒤로가기 버튼이 올바르게 작동하도록 표준 3xx HTTP 상태 코드를 사용하는 것이 좋다.

    기억해야 할 것은 리다이렉션이 사용자 경험을 느리게 한다는 것.

    사용자와 HTML 문서 사이에 리다이렉션을 삽입하면 페이지의 아무것도 렌더링 할 수없고, HTML 문서가 도착할 때까지 구성 요소를 다운로드 할 수 없으므로 페이지의 모든 항목이 지연된다.

     

    문제는 낭비적인 리다이렉션이 자주 일어나지만 일반적인 개발자는 인식하지 못하는 경우다.
    예를들어 URL에서 trailing slash (/)가 누락 된 경우 발생한다.

    쉽게 설명하면 http://example.com의 요청시 http://example.com/ (trailing slash 포함)의 리디렉션을 포함하는 301 응답 결과가 전송된다. 

     

    예전 사이트를 새로운 사이트에 연결하는 것도 리다이렉션의 또 다른 일반적인 용도다.

    웹 사이트의 다른 부분을 연결하고 특정 조건 (기기와 브라우저 유형, 사용자 계정 유형 등)에 따라 사용자를 안내하는 것도 있다.

     

    리다이렉션을 사용하여 두 웹 사이트를 연결하는 것은 간단하며 추가 코딩이 거의 필요하지 않다.

    하지만 개발자의 복잡성이 줄어드는 반면 사용자 환경이 저하된다는 점은 반드시 염두해두어야 한다.

     

    해결방법

    - trailing slash

    이것은 Alias, mod_rewrite, DirectorySlash 지시어를 사용함으로써 Apache에서 해결된다.

     

    - 리다이렉션 성능 개선

    Redirects 사용에 대한 다른 대안은 두 code 경로가 같은 서버에 host되는 경우, Alias와 mod-rewrite를 사용하는 것이다. 도메인 이름변경을 위해 redirects를 사용한다면 Alias 또는 mod_write를 결합하여 CNAME(DNS record: 하나의 도메인 이름을 다른 것으로 가리키는 별칭도메인을 생성)를 생성하면 된다.

     

    - 반응형 디자인

    적응형 디자인을 적용하기 위해 리다이렉트 하는 경우가 있다.

    모바일 사이트를 분리하는 경우가 대표적이다.

    • example.com
    • example.com -> m.example.com

     

    만약 반응형 디자인을 도입한다면 리다이렉트 없이 데스크탑, 모바일에 맞는 사이트를 만드는 것이 가능하다.

     

    이와 별도로 적응형 웹은 별도 URL, 반응형 웹은 반응형 웹 디자인 기본 사항이 입문에 좋은 문서같다. [적응형 웹 디자인 VS 반응형 웹 디자인, 반응형 웹을 위한 레이아웃 설계 방법(1편, 2편)]

     

    2.2.5 iframe 수를 최소화

    개요

    분류: content
    Iframe을 사용하면 HTML 문서를 부모 문서에 삽입 할 수 있다.

    Iframe이 효과적으로 사용되도록 작동하는 방식을 이해하는 것이 중요하다.

     

    iframe의 단점은 다음과 같다.

    • 비어있더라도 비쌈[Using Iframes Sparingly]
    • page onload를 차단함
    • 대역폭을 메인 페이지와 경쟁
    • Non-semantic (비의미론적이다)
    • 기타 보안 사항에 취약

     

     

    해결방법

    - Ajax 활용

    Ajax를 이용해 데이터를 로드해서 div에 결합할 수 있다.[Best approach to replace iframes]

     

    - 대체 태그 사용??

    다른 방식으로 삽입을 하는 것을 생각해보자면 objectembed란 태그가 있다.

    그러나 From object to iframe — other embedding technologies, difference between iframe, embed and object elementsWhat is the difference between <iframe>, <object> and <embed>?, <iframe> vs <object> vs <embed>와 같은 글을 읽어보면 딱히 대체품이 될 수 없을 듯 하다.

     

    HTML5의 audio, video, web components 가 대안이 될 수 있겠다.

     

    구글은 iframe 대신 potal이란 태그를 발표하기도 했지만 이제 막 논의될랑 말랑 하기 때문에 무시해도 좋다.

    [Google launches Portals, a new web page navigation system for Chrome, Hands-on with Portals: seamless navigation on the Web]

     

    2.2.6 document.write() 사용하지 않기

    개요

    분류: Javascript

    document.write()는 사이트를 느리게 만들 수 있다. [Intervening against document.write(), Why you should avoid using document.write, specifically for scripts injection, Don't docwrite scripts]

     

    특히 document.write()를 통해 동적으로 스크립트를 삽입한다면 매우 느려질 수 있다.

    document.write('<script src="https://paul.kinlan.me/ad-inject.js"></script>');

    HTML 파싱을 중단하고, 리소스가 로드되어 실행 될때까지 기다려야 하기 때문인데, 크롬의 경우 차단하기 까지도 한다.

     

    DOM 트리가 만들어진 후, document.write가 사용되면 다시 빌드를 해야하므로 역시 성능에 좋지 않다.

     

    해결방안

    스크립트 로드는 앞서 나온 defer, async로 하며, 굳이 자바스크립트로 해야 하다면 DOM 조작을 통해서 하면 된다.

    const sNew = document.createElement ( "script"); 
    sNew.async = true; 
    sNew.src = "https://example.com/script.min.js"; 
    const s0 = document.getElementsByTagName ( 'script') [0]; 
    s0.parentNode.insertBefore (sNew, s0);

     

    2.2.7 textContent 사용

    개요

    분류: Javascript

    자바스크립트에서 텍스트를 다룰때 textContent, innerText, innerHTML 3가지를 생각해볼 수 있다. [당신이 innerHTML을 쓰면 안되는 이유

     

    이 중 textContent의 성능이 가장 좋은 편이다.

    • innerText: CSS를 고려하기 때문에 innerText값을 읽으면 최신 계산값을 반영하기 위해 리플로우가 발생
    • innerHTML: HTML로 분석하는 과정 필요

     

    특히, innerHTML은 성능이 나쁘고, XSS(Cross-Site Scripting)에 취약해 사용하지 않는 것이 좋다.

     

    2.2.8 CSS 선택자

    개요

    분류: CSS

    CSS 작성을 효율적으로 하면 스타일 계산을 줄일 수 있다. [스타일 계산의 범위와 복잡성 줄이기]

    특히 스타일 계산시 50%는 선택자를 매칭하는데 사용하기 때문에 크게 줄일 수 있다. [Blink에서 스타일 무효화]

    CSS 셀렉터는 오른쪽부터 읽으며, 실패하는게 좋다. (중복 최소화)

     

    선택자별 성능차

    셀렉터별로 성능차가 존재한다. [CSS Selectors: Should You Optimize Them To Perform Better?, Efficiently Rendering CSS, Writing Efficient CSS for use in the Mozilla UI]

    id가 가장 빠르고, 가상 클래스가 가장 느리다.

    1. id (#myid)
    2. class (.myclass)
    3. tag (div, h1, p)
    4. adjacent sibling (h1 + p)
    5. child (ul > li)
    6. descendent (li a)
    7. universal (*)
    8. attribute (a[rel=”external”])
    9. pseudo-class and pseudo element (a:hover, li:first)

     

    선택자 복잡성

    예를 들어 다음은 box의 마지막의 $-n+1$번째를 상위요소로 둔 .title 클래스를 고르는 것이다.

     

    .box:nth-last-child(-n+1) .title {
      /* styles */
    }

     

    만약 다음처럼 하면 계산이 줄어든다.

    .final-box-title {
      /* styles */
    }

     

     

    규칙

    구조적으로 CSS를 작성하면 선택의 복잡성이 줄어들 수 있다.

    예를 들어 BEM은 단일 클래스 계층을 가지기 때문.

    .list { }
    .list__list-item { }

    BEM 형식의 코드

     

    다음은 CSS 방법론들 [[번역] 공룡을 위한 모던 CSS, [번역] CSS개선을 위한 SEM과 BIO의 결합, Happy with your CSS files in your big app?, CSS 방법론, Atomic Design System architecture with ITCSS, BEM, SASS, Journey to Enjoyable, Maintainable Styling with React, ITCSS, and CSS-in-JS, Methods to Organize CSS]

     

    일단 JSS 계열에서 Styled Components/Emotion, AphroditeStyletron이 눈에 띄는 것 같다.

    일반 CSS in JS vs Styletron

    특히 Styletron의 atomic 방식은 크기를 줄여줄 수 있어 특이한 듯(대신 셀렉터가 늘어나 렌더링 성능과 트레이드오프)

     

    JS를 이용해 CSS를 생성하는 방식은 런타임시에 영향을 미칠 수 있는데 [The unseen performance costs of modern CSS-in-JS libraries in React apps, Styling? with Linaria!, How is Linaria different from Emotion and Styled Components, The tradeoffs of CSS-in-JS]

    Linaria(장점)를 사용하면 정적으로 생성할 수 있든 듯 하다.

    SASS와 PostCSS도 사용가능한 것 같고.

     

    최근에 css-blocks란 프로젝트를 찾았다.

    CSS 모듈, BEM, Atomic CSS의 장점을 모두 활용하려는 듯.

     

    마이크로소프트에서 만든 Griffel 또한 CSS-In-JS, AOT Compile, Atomic CSS를 지원하는 강력한 라이브러리다.

     

    2.2.9 IE 독점 CSS

    개요

    분류: CSS

     

    다음은 IE 독점 CSS 최적화에 관련된 내용이다.

     

    - CSS 표현식 피하기

    CSS 표현식은 CSS 속성을 동적으로 설정하는 강력하고 위험한 방법이다. 

    버전 5부터 Internet Explorer에서 지원되었지만 IE8부터는 더 이상 사용되지 않으므로 신경 쓸 필요는 없다.

     

    예를 들어 CSS 표현식을 사용하여 매시간마다 배경색을 번갈아 설정할 수 있는데

    background-color: expression( (new Date()).getHours()%2 ? "#B8D4FF" : "#F08A00" );

     

    expression은 JavaScript 표현식을 사용하며 CSS 속성은 JavaScript 표현식을 평가 한 결과로 설정된다.

    이 expression방법은 다른 브라우저에서 무시되므로 Internet Explorer에서 속성을 설정하여 브라우저간에 일관된 환경을 만드는 데 유용했다.

    expression의 문제점은 대부분의 사람들이 기대하는 것보다 더 자주 평가된다는 것.

    페이지가 렌더링되고 크기가 조정될 때뿐만 아니라 페이지가 스크롤 될 때와 사용자가 페이지 위로 마우스를 움직일 때도 평가된다.

    CSS 표현식에 카운터를 추가하면 CSS 표현식이 언제 그리고 얼마나 자주 평가되는지 추적 할 수 있다.

    페이지 주위로 마우스를 이동하면 10,000 개가 넘는 평가를 쉽게 생성 할 수 있다.

     

    -  AlphaImageLoader 필터 피하기

    AlphaImageLoader필터는 IE 버전 7이하에서 반투명 트루 컬러 PNG의 문제를 해결하는 것을 목표로 한다.

    모던 브라우저에서 신경 쓸 필요 없다는 뜻.

     

    이 필터의 문제점은 이미지를 다운로드하는 동안 렌더링을 차단하고 브라우저를 정지시키는 것이다.

    또한 메모리 소비가 증가하고 이미지가 아닌 요소별(element)로 적용되므로 문제가 배가 된다.

     

     

    해결방법

    - CSS 표현식 피하기

    CSS 표현식의 평가 횟수를 줄이는 한 가지 방법은 일회성 표현식을 사용하는 것이다. 

    여기서 표현식을 처음 평가할 때 스타일 특성을 명시 적 값으로 설정하여 CSS 표현식을 대체한다.

     

    스타일 속성을 페이지 수명 동안 동적으로 설정해야하는 경우 CSS 표현식 대신 이벤트 핸들러를 사용하는 것이 대안이다.

    CSS 표현식을 사용해야하는 경우 수천 번 평가 될 수 있으며 페이지 성능에 영향을 줄 수 있다는 것을 명심해야 한다.

     

    -  AlphaImageLoader 필터 피하기

    가장 좋은 방법은 AlphaImageLoader를 완전히 사용하지 말고, PNG8을 사용하는 것. 

    꼭 필요한 경우 hack_filter를 사용하여 IE7 + 사용자에게 불이익을 주지 않도록 해야한다.

     

    2.3 효율적 활용

    2.3.1 Streams

    개요

    분류: Javascript

    Streams를 이용하면 성능 향상의 여지가 있다. [2016년은 웹 스트림(web stream)의 해다., A Guide to Faster Web App I/O and Data Operations with Streams]

     

    특히 커다란 요소를 점진적으로 렌더링을 하거나, 가져와서(fetch) 변환해야 할 때 유용하다.

     

    해결방법

    MDN의 stream 예제는 이미지를 그레이스케일로 바꾸거나, 커다란 이미지를 청크로 나누어 전송하는 것을 보여준다.

     

    오프라인 위키피디아 데모(깃허브)에 적용되었다고 하는데 후술할 서비스 워커와 함께 사용해 상당히 좋은 성능을 보여준다. [Stream Your Way to Immediate Responses]

    관심이 간다면 깃허브 소스를 확인해 볼 수 있다.

     

    streaming-css처럼 점진적으로 CSS를 적용하도록 만들 수도 있긴한데,  전체를 로드하는 시간이 더 빨라 좋은 방법이 아니다.

    React는 ReactDOMServer에서 스트림을 지원하는 듯. 조금 오래된 react-dom-stream도 있다.

     

    2.3.2 iframe 활용

    개요

    분류: Content

    iframe은 단점이 많지만 iframe도 장점이 있다.

    • 배지(badges)나 광고와 같이 느린 third-party content를 원활하게 함
    • Security sandbox (보안 sandbox)
    • scripts를 병렬로 다운로드 함

     

    해결방법

    - Non-Blocking 로드

    앞서 iframe의 단점의 주요 원인은 다음과 같다.

    • onload 이벤트가 발생하기 전에 iframe 로드
    • src 속성은 다운로드 받아야 하는 리소스

    따라서 onload 전에 iframe이 오는 것를 막거나(onload 이벤트 이후 비동기적 로드), src 속성없이 iframe을 사용한다면 성능상 불이익은 없다.  [Using Iframes to Address Third-Party Script Issues and Boost Performance(번역)]

     

    리소스 힌트가 먹히지 않는 구형 브라우저를 사용한다면 iframe을 이용해 로드를 시도해보자.[A CSP Compliant non-blocking script loader, Non-Blocking Loader Snippet, Loading Third-Party JavaScript]

    <script id="nb-loader-script">
    (function(url) {
    
      // document.currentScript works on most browsers, but not all
      var where = document.currentScript || document.getElementById("nb-loader-script"),
          promoted = false,
          LOADER_TIMEOUT = 3000,
          IDPREFIX = "__nb-script";
    
      // function to promote a preload link node to an async script node
      function promote() {
        var s;
        s = document.createElement("script");
        s.id = IDPREFIX + "-async";
        
        s.src = url;
    
        where.parentNode.appendChild(s);
        promoted = true;
      }
    
      // function to load script in an iframe on browsers that don't support preload hints
      function iframe_loader() {
        promoted = true;
        var win, doc, dom, s, bootstrap, iframe = document.createElement("iframe");
    
        // IE6, which does not support CSP, treats about:blank as insecure content, so we'd have to use javascript:void(0) there
        // In browsers that do support CSP, javascript:void(0) is considered unsafe inline JavaScript, so we prefer about:blank
        iframe.src = "about:blank";
        
        // We set title and role appropriately to play nicely with screen readers and other assistive technologies
        iframe.title = "";
        iframe.role = "presentation";
        
        s = (iframe.frameElement || iframe).style;
        s.width = 0; s.height = 0; s.border = 0; s.display = "none";
        
        where.parentNode.insertBefore(iframe, where);
        try {
          win = iframe.contentWindow;
          doc = win.document.open();
        }
        catch (e) {
          // document.domain has been changed and we're on an old version of IE, so we got an access denied.
          // Note: the only browsers that have this problem also do not have CSP support.
          
          // Get document.domain of the parent window
          dom = document.domain;
          
          // Set the src of the iframe to a JavaScript URL that will immediately set its document.domain to match the parent.
          // This lets us access the iframe document long enough to inject our script.
          // Our script may need to do more domain massaging later.
          iframe.src = "javascript:var d=document.open();d.domain='" + dom + "';void(0);";
          win = iframe.contentWindow;
          doc = win.document.open();
        }
    
        bootstrap = function() {
          // This code runs inside the iframe
          var js = doc.createElement("script");
          js.id = IDPREFIX + "-iframe-async";
          js.src = url;
          doc.body.appendChild(js);
        };
        
        try {
          win._l = bootstrap
    
          if (win.addEventListener) {
            win.addEventListener("load", win._l, false);
          }
          else if (win.attachEvent) {
            win.attachEvent("onload", win._l);
          }
        }
        catch (f) {
          // unsafe version for IE8 compatability
          // If document.domain has changed, we can't use win, but we can use doc
          doc._l = function() {
            if (dom) {
              this.domain = dom;
            }
            bootstrap();
          }
          doc.write('<bo' + 'dy onload="document._l();">');
        }
        doc.close();
      }
    
      // We first check to see if the browser supports preload hints via a link element
      var l = document.createElement("link");
    
      if (l.relList && typeof l.relList.supports === "function" && l.relList.supports("preload") && ("as" in l)) {
        l.href = url;
        l.rel  = "preload";
        l.as   = "script";
        
        // If the link successfully preloads our script, we'll promote it to a script node.
        l.addEventListener("load", promote);
        
        // If the preload fails or times out, we'll fallback to the iframe loader
        l.addEventListener("error", iframe_loader);
        setTimeout(function() {
            if (!promoted) {
                iframe_loader();
            }
        }, LOADER_TIMEOUT);
        
        where.parentNode.appendChild(l);
      }
      else {
        // If preload hints aren't supported, then fallback to the iframe loader
        iframe_loader();
      }
    
    })("https://your.script.url/goes/here.js");
    </script>

     

    - Iframe + document.write()을 이용한 스트리밍

    역시 구 브라우저를 위한 트릭.[웹 페이지에서 컨텐츠를 빠르게 보여주기 위한 트릭]

    최신 브라우저의 서비스 워커를 사용하면 좋다. [streaming-html (github)]

    서비스워커는 알아야 할 내용이 많아서 따로 다루기로 한다.

     

    모든 내용을 다 받고 나서 보여주기보다는 점진적으로 보여주는 것이 좋다.

    하지만 아래 코드는 page-data.inc의 내용을 다운로드 받을 때까지 기다려야 한다.

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

     

    iframe과 document.write() 사용하면 다운로드 받는 내용들을 바로 보여줄 수 있다.

    // 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 = '';

     

    그러나 document.write()는 성능에 나쁜 요소가 될 수 있다.

    따라서 생각해볼만한 것은 JSON 형식으로 가져오는 것인데 보통 우리가 생각하는 JSON은 다음과 같이 생겼다.

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

    하지만 스트리밍에 친화적인 포맷이 아니라 스트리밍 JSON 파서를 필요하고 사용법도 까다롭다.

     

    하지만 newline-delimited JSON이라 하여 개행으로 이루어진 JSON을 이용하면 파싱이 훨씬 간단해진다.

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

     

    splitStream과 parseJSON은 재사용 가능한 변화되는 스트림이며, xhr을 사용할 수도 있다. [parse-json.js, xhr-ndjson]

    // 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);
    }

     

    벤치마크 결과를 보면 안티패턴으로 알려진 iframe + document.write()가 의외로 빠르다는 것을 알 수 있다.

    가독성이 나쁘고, 고려할 요소가 많은 것이 문제지만, 신중히만 사용하면 쓸모가 있을지도.

    물론, 되도록 사용하지 않는 것이 우선이다.

    그낭 서비스 워커 + 스트리밍을 쓰자.

     

    2.3.3  웹 컴포넌트의 최적화 가능성?

    개요

    분류: Content

    웹 컴포넌트는 3개(Import까지 포함했다면 4가지)로 이루어져 있다. [웹 컴포넌트, TOAST UI - 웹 컴포넌트 연재,  An Introduction to Web Components]

    • Custom elements: HTML 요소 및 해당 동작을 정의 및 확장 [Custom Elements]
    • Shadow DOM: 컴포넌트의 스코프를 분리 [Shadow DOM 101]
    • HTML Templete: 렌더링된 페이지에 나타나지 않은 마크업으로, 커스텀 엘리먼트 구조를 기반으로 재사용 가능 [HTML's New Template Tag]
    • HTML Import: 다른 HTML 파일에서 템플릿을 가져올 수 있음 [HTML ImportsStackoverflow]
      단, 현재 분위기상 HTML Import는 제외되는 것 같다.

     

     

    그럼 웹 컴포넌트에서 어디가 최적화 가능성이 있을까?

    안타깝게도 현재는 Native에 비해 느린 편이지만 [Styling Web Components Using A Shared Style Sheet]

     

    앵귤러보다 빠르고 [Simplifying Performance with Web Components]

    비교적 최신기술이라 최적화가 덜 이루졌을 것이란 점을 고려하면 기대중이다.

     

     

    웹 컴포넌트를 잘 쓰도록 만들어진 라이브러리에는 [7 Tools for Developing Web Components in 2019]

    등이 있는데 Vue.JS는 최적화가 부족한 모양. [Stencil.js vs lit-element vs Vanilla vs Shadow DOM vs Vue.js What is the best solution for Web component]

     

    Constructable Stylesheet Objects

    Constructable Stylesheet Objects는 최초 한번 파싱 후, 공유하는 규칙들을 Shadow DOM을 이용해 적용(Attach)하는 방식으로 재사용이 가능해 성능에 도움을 줄 수 있다. [Constructable Stylesheets: seamless reusable styles, Adopt a Design System inside your Web Components with Constructable Stylesheets, Getting Adoption for Design Systems — A Practical Guide, A Quick Look at Constructable Stylesheets]

     

    댓글

Designed by black7375.