ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스압/데이터주의] 웹 최적화 방식 모음
    프로그래밍/Web 2020. 3. 17. 15:56

    웹으로 전향한지 얼마 안됐는데

    그동안 경험상 전반적 구조 파악에는 퍼포먼스 개선하는게 짱짱이라 적게되었습니다.

    이 문서는 완전히 완성된 상태가 아니라 시간이 나면 업데이트 중.

    혹시 이외에도 재미있는 팁을 아시는 분이 있다면 댓글로 알려주세요.

    모바일이나 기타 프로그래밍/전공 경험과 지식을 살려봤습니다.

     

    제 블로그 글들을 보면 알겠지만 상당히 장문으로 이루어져 있지만..

    이건 역대급으로 심하게 길다보니 머릿글을 ㅎㅎ

    조금 과장해서 책 한권;;

     

    블로그 스킨을 React 기반으로 클린룸 구현해볼까 생각 중이라 더 열심히 하고 있습니다.

    타입스크립트, PWA, 웹컴포넌트 등 파워풀한 것을 모두 넣어서 가볍고 빠르고 생산성까지 잡으며 아름답게 만드는 것이 목표 ㅎㅎ

     

    0. 전반적 원칙과 원리

    이 긴 글을 읽기 귀찮다면 TOAST UI팀이 작성한 성능 최적화을 읽어보면 좋다.

    아주 깔끔하고 세세하게 설명해놓았다.

     

    0.1 이 글의 원칙

    성능 개선을 위해서는 적절한 알고리즘과 자료구조를 사용하는 것이 좋다.

    원래 그러라고 나온 것들이니까.

     

    하지만, 이 글은 알고리즘과 자료구조를 소개하는 글은 아니므로 제외.

    네트워크, 브라우저, 언어적 구조와 특성을 이해하고, 최적화하는 것에 중점을 두고 소개한다.

     

    효율적인 성능 개선을 위해서 구조에 대한 파악과, 측정은 중요하다.

    성능은 측정 가능해야 한다.

     

    최신 기술인 SPA나 웹번들러 관련 기법들은 가장 보편적인 리엑트, 웹팩 기준으로 서술한다.

     

    0.2 원리

    0.2.1 네트워크 동작

    네트워크 동작은 네트워크 시간에 흔히 배우는 DNS 개념과 TCP/IP에 대한 개념(주로 핸드쉐이크)에 대한 개념이 필요하다.

    High Performance Browser Networking에서 전반적 내용을 잘 설명해주고 있다.

     

    - DNS

    아마존의 DNS란 무엇입니까?란 글이 쉽게 설명해준다.

    조금 더 자세한 글을 원한다면 클라우드플레어의 글을 참고하면 좋다.

     

    1. 브라우저에서 www.example.com 같은 도메인 이름을 입력
    2. ISP가 운영하는 DNS Resolver(또는 Local DNS)로 쿼리를 수신
    3. Local DNS에 IP주소가 있다면 전달, 아니라면 루트 DNS 서버에 전달
    4. .com, .net, .kr처럼 공통 도메인 확장자를 관리하는 TLD(Top-Level Domain) 서버에 전달
    5.  TLD 서버는 example.com을 관리하는 DNS 서버에 요청
    6. IP를 전달 받고
    7. 브라우저에게 알려줌
    8. 전달받은 IP주소를 기반으로 요청
    9. www.example.com이 해당되는 웹페이지 반환

    - TCP/IP 연결

    TLS (Transport Layer Security)

     

    TCP는 흔히 알다시피

    • SYN
    • SYN ACK
    • ACK

    3 handshake로 이루어진다.

     

    그리고 보안에 필요한 TLS까지 사용한다면 몇단계가 더 추가된다. [What Happens in a TLS Handshake?, TLS 핸드쉐이크 (Handshake) 프로토콜 분석, The Illustrated TLS Connection]

     

     

    0.2.2 브라우저 동작

    다음 글은 브라우저가 어떻게 동작하는지 잘 설명해주고 있다.

     

    유명한 브라우저 엔진인 웹킷과 게코의 렌더링 과정은 이렇다.

     

    살짝 차이가 있긴 하지만 다음의 과정을 거친다고 볼 수 있다.

    다운로드 - 파싱 - 스타일링 - 레이아웃 - 페인트 -합성&렌더링

     

    이중 주목할 것은 Layout과 Paint에서 많은 리소스가 필요하다는 것이다.

    그래서 돔 조작을 줄이기 위해 Virtual Dom과 같은 개념을 사용하는 라이브러리들이 나오는 중.

     

    이번에는 조금 다른 방향으로 들어가서 자바스크립트 엔진의 소개글을 읽어볼 수도 있다.

     

    최신 브라우저들의 구조에 대해서는

    를 읽어보길 바란다.

    왜 메인 쓰레드, 컴포지터 쓰레드, GPU 가속등으로 처리를 나누어 사용하고, 서비스 워커와 웹 어셈블리가 생겨났는지 이유를 찾을 수 있다.

     

    마지막으로 하드웨어 가속관련 글

     

    브라우저의 각종 기술의 지원 여부는

    https://caniuse.com/

     

    Can I use... Support tables for HTML5, CSS3, etc

    About "Can I use" provides up-to-date browser support tables for support of front-end web technologies on desktop and mobile web browsers. The site was built and is maintained by Alexis Deveria, with occasional updates provided by the web development commu

    caniuse.com

    를 참고바란다.

     

    0.2.3 측정 및 해석

    - 브라우저 내장 프로파일러

    가장 먼저 생각해볼 수 있는 것은 TOAST UI팀이 작성한 성능 최적화처럼 브라우저에 내장된 프로파일러를 가지고 성능을 측정할 수 있다.

     

    프로파일러에서 보이는 각종 타이밍은 다음과 같다. [Navigation and resource timings]

     

    다루는 방법은 Chrome DevTools, Firefox 개발자 도구, Safari Developer Help, Microsoft Edge(EdgeHTML) Developer Tools 문서를 참고.

     

    크로미움(Blink), 파이어폭스(Getko), 미도리(Webkit2GTK)의 프로파일링 모습.

    요즘 맥북을 잘 안쓰고 리눅스에서 작업하느라 사파리 대신 넣었다.

    웹킷은 정보가 너무 부족하고, 크로미움이나 파이어폭스는 장단점이 존재하는데..

     

    가장 좋은 것을 뽑자면 크로미움이다.

    하드웨어나 각 모듈, 이벤트별로 꼼꼼하게 리소스 소모/부하 및 UX적으로 느끼는 향상까지 전반적으로 다룰 수 있기 때문이다.

    • FPS, CPU, GPU, Network,  Compositor, Service Worker, 자바스크립트 프레임 차트등 다양한 부하
    • 스크린샷, 매 프레임의 시각화
    • 또한 점진적 렌더링과 사용자 기준의 성능측정 기준인 FP(First Paint), FCP(First Contentful Paint), FMP(First Meaningful Paint)등의 이벤트 시간

    이외에 어플리케이션 탭에서 PWA 수준까지 정보 제공, Audits에 내장된 구글 라이트하우스 프로파일링 등 매우매우 강력한 디버깅을 제공한다.

     

    파이어폭스의 경우,

    크롬처럼 각 리소스별 세부적인 정보보단, 우리에게 익숙한 렌더링 프로세스에 충실한 워터풀 모델이 특징.

    • setTimeout, 이벤트 핸들러, 함수호출 등의 자바스크립트
    • 마이너 GC, 비증가형 GC, 사이틀 콜렉션등 자바스크립트 엔진
    • 스타일 재계산, 변경적용, 레이아웃등의 CSS
    • 페인트

     

    웹킷은 애플이 만드는 것 답게 매우우 심플하며 어떤 부분에서 개선이 필요할지 대략 감잡기 쉽다.

    그런데 위에서 적었다시피 정보가 너무 부족;;

    특이한 것은 레이어를 볼 수 있다는 것 정도?(아마 Graphics Layer를 뜻하는 듯 하다, 그래픽 가속에서 살짝 다룸)

     

    단, 메모리 관리나 레이아웃 디버깅은 파이어폭스가 쉬운편.

    시각화가 잘 되어 있고, 보기를 바꾸면 크롬만큼 자세히 보는 것도 가능

     

     

    엄청나게 직관적인 CSS 플렉스 박스와 그리드 다루기, 폰트, 애니메이션 등등의 기능

     

     

    - 라이브러리/프레임워크 전용 프로파일러

     

    각종 라이브러리나 프레임워크 전용 프로파일러, 개잘자도구들이 존재한다.

     

    Redux나 MobX같은 상태관리 라이브러리도 개발자 도구를 지원한다.

    웹팩을 사용하면

    를 애용하자.

     

    - 기타 측정

    다른 측정 방법 중 가장 유명한 것은 구글의 PageSpeed Insights와 라이트하우스(Lighthouse)다.

    라이트하우스의 진화: CI, 새로운 성능 지표 등도 재미있는 문서.

     

    라이트하우스는 크롬 개발자도구 또는

    를 활용해보자.

     

    다음은 기타 성능 측정 사이트가 잘 정리되어 있는 곳이거나 도구이다.

    +

    구글의 Search Console 속도 보고서를 보면 성능 측정 후 적용/활용에 대한 이해에 조금이나마 도움이 되지 않을까 싶다.

     

     

    1. 다운로드

    사용자(End-user) 응답시간의 80%는 초기 로딩부분에서 소요된다. 그 중 대부분의 시간은 모든 페이지 구성요소(images, syltesheets, scripts 등)의 다운로드에 소요된다.

    따라서 다운로드 관련 시간을 아끼는 것은 중요하다.

     

    크게 3~4가지의 기준으로 나누어볼 수 있다.

    줄이기(용량, 요청), 합치기, 나누기

     

    1.1 용량 줄이기

    리소스의 용량을 줄이면 다운로드에 걸리는 시간이 줄고, 해석하는데 시간 또한 줄어들기 때문에 최적화에 이점이 있다.

    필요한 데이터 양이 초기 혼잡 윈도우(일반적으로 14.6 KB 압축)를 초과하면 서버와 사용자 브라우저 간에 추가 왕복이 필요하기도 하다.

    모바일 네트워크와 같이 지연 시간이 긴 네트워크를 사용하는 사용자의 경우 이로 인해 페이지 로드가 크게 지연될 수 있다. 

     

    1.1.1 디자인 단순화

    개요

    분류: Content

    디자인을 단순하게 만들면 다운받아서 보여줄 요소들이 줄어들겠죠?

    간단히 Dom 요소(element)를 줄이자고 생각해봅시다.

    복잡한 페이지는 다운로드시 용량이 더 커지며 Javascript에서 DOM 엑세스 속도도 느려지게 만들 수 있다.

    예를 들어 이벤트 핸들러를 추가할 때 500 VS 5000개의 DOM 요소를 반복해야 한다면 누가 더 빠를까?

     

    보통 다음을 넘지 않는 것이 권장사항이다. [Uses An Excessive DOM Size]

    • DOM 트리 노드: 1500
    • 최대 깊이: 32
    • 자식노드를 가지는 부모노드: 60

     

    Browser Cache Usage - Exposed! 를 보면 40-60% 의 방문자가 브라우저 캐쉬의 혜택을 보지 못한다고 한다. 따라서 static 파일이라고 해도 그 수를 줄이는 것이 방문자에게 체감속도를 높이는 방법이다.

     

    측정방법

    DOM 요소의 수는 웹콘솔에 다음과 같이 입력하면 쉽게 알 수 있다.

    document.getElementsByTagName('*').length

     

    해결방법

    레이아웃을 만들기 위해 DOM 요소를 남용했을 수도 있다.

    CSS를 잘 활용하고, 중첩되는 것들을 최대한 줄여보자.

     

    예를 들어 옛날에는 레이아웃을 위한 CSS가 없거나 제대로 적용되지 않는 브라우저가 많았기 때문에 <table>을 사용했었는데 중첩적으로 사용해야 했기 때문에 DOM 요소는 늘어날 수 밖에 없었다.

    요즘은 Flexbox Grid 같이 매우 편리한 CSS Layout이 많다.

     

    +. 이외의 <table> 태그의 문제점

    <table> 태그는 전체가 하나의 객체이기 때문에 모든 셀(cell)이 로딩되어야만 보여준다.

    <div> 태그는 각각이 하나의 객체라 점진적으로 렌더링이 가능한것과 대조적.

    코드 수정, 컨텐츠 배치가 힘든 것도 단점.

     

    ++.

    안드로이드나 IOS의 경우 깊이를 낮추기 위해 Constraint Layout이나 Auto Layout을 사용하는데 성능에 좋다.[Constraint Layout performance(번역), 안드로이드 Layout별 성능 비교High Performace Layout(요약)]

     

    1.1.2 가벼운 라이브러리와 모듈

    개요

    분류: Content

    기능에 비해 쓸데없이 비대한 라이브러리보다 가벼운 라이브러리를 사용하면 크기가 줄어든다.

    또한 필요한 함수만 사용해야 한다.

    실제로 의존성을 줄이는 것은 효과적인 최적화 방법이다.[Kill Your Dependencies]

     

    측정방법

    Github jsDelivr에서 min을 기준으로 확인해본다.

    앞서 소개한 Webpack Bundle Analyzer로 쓸데없는 종속성을 탐색

     

    해결방법

    - 가벼운 라이브러리

    셀렉터($)만 사용하는데 jQuery를 사용한다면 바닐라로 대체하거나 Cash(cash-dom, 타입스크립트도 잘 지원)을 이용한다거나 React 대신 Preact,

    간단한 디자인의 사이트를 만드는 거라면 Bootstrap이나 Foundation을 사용하기 보다 Bulma Spectre 더 가벼운 것을 원한다면 Pure CSS Skeleton이 있겠다.

     

    - 필요한 함수만

    import _ from 'lodash';
    
    _.array(...);
    _.object(...);

    보다는

     

    import array from 'lodash/array';
    import object from 'lodash/fp/object';
    
    array(...);
    object(...);

    처럼.

     

    1.1.3 중복 스크립트 제거

    개요

    분류: Javascript

    동일한 JavaScript 파일을 한 페이지에 두 번 포함하면 성능이 저하된다.

    이 경우 중복 스크립트는 불필요한 HTTP 요청을 생성하고 JavaScript 실행을 낭비하여 성능을 저하시킨다.

     

    불필요한 HTTP 요청은 구형 Internet Explorer에서는 발생하지만 최신 브라우저들에서는 발생하지 않는다.

    구형 Internet Explorer에서 외부 스크립트가 두 번 포함되고 캐시 할 수없는 경우 페이지로드 중에 두 개의 HTTP 요청이 생성된다.

    스크립트가 캐시 가능하더라도 사용자가 페이지를 다시로드하면 추가 HTTP 요청이 발생한다는 것.

     

    불필요한 HTTP 요청을 생성하는 것 외에도 스크립트를 여러 번 평가하는 데 시간이 낭비된다.

    이 중복 JavaScript 실행은 스크립트가 캐시 가능한지 여부에 관계없이 모두에서 발생한다.

     

    중복된 스크립트의 가능성을 높이는 두 가지 주요요인은 팀 규모와 스크립트 수이다.

    YDN의 글에 따르면 미국의 상위 10개 사이트 중 2개에서 발생하던 일이라 생각보다 드물지 않다는데 요즘은 빌드나 관리도구의 발전으로 덜할거라 예상한다. ㅎㅎ

    하지만 예전 방식으로 만들어서 운영되는 사이트도 많을테니 아예 말이 안되는 이야기는 아니다.

     

    측정방법

    gtmetrix를 이용해보자.

    const scripts = document.getElementsByTagName("script");
    const scriptsObj = [...scripts].map(script => script.src)
      .filter(src => src !== "")
      .reduce((obj, value) => {
        if (obj.hasOwnProperty(value)) {
          obj[value] = obj[value] + 1;
        } else {
          obj[value] = 0;
        }
        return obj;
      }, {});
    Object.keys(scriptsObj).forEach(key => {
      if (scriptsObj[key] > 0) delete scriptsObj[key];
    });

    를 웹콘솔에 입력해보아도 <script>의 중복된 갯수를 알 수 있다.

     

    해결방법

    중복 스크립트가 생기는 원인은 팀 규모와 스크립트 수라 하였다.

    그렇다면 체계적인 시스템을 만들면 되겠죠?

     

    - 이름 관리

    실수로 동일한 스크립트를 두 번 포함하는 것을 피하는 한 가지 방법은 템플릿 시스템에 스크립트 관리 모듈을 구현하는 것입니다.

    대표적인 예는 버전을 달아놓는 것.

    <script type = "text / javascript"src = "menu_1.0.17.js"> </ script>

     

    PHP의 대안은 insertScript이라는 함수를 만드는 것 이다.

    <? php insertScript ( "menu.js")?>

     

     

    동일한 스크립트가 여러 번 삽입되는 것을 방지 할뿐만 아니라 종속성 검사 및 버전 번호를 스크립트 파일 이름에 추가하여 향후 캐시 제어문 헤더를 지원하는 등 스크립트의 다른 문제를 처리 할 수 ​​있다.

    리소스 이름 관리는 캐시 제어에서 더 알아보도록 한다.

     

    - 모듈 관리

    자주 사용하는 함수들은 모듈로 묶어 관리하고, CSS는 SCSS를 사용하면 변수와 믹스인을 사용하여 역시 중복을 줄일 수 있다.

     

    // foo.js
    function filter() { ... }
    function map() { ... }
    
    filter();
    map();
    
    
    // bar.js
    function filter() { ... }
    function find() { ... }
    
    filter();
    find();

    에서

     

    // utils.js
    export function find() { ... }
    export function filter() { ... }
    export function map() { ... }
    
    
    // foo.js
    import {filter, map} from 'utils.js'
    
    filter();
    map();
    
    
    // bar.js
    import {filter, find} from 'utils.js'
    
    filter();
    find();

    가 되는 식이다.

     

    1.1.4 JavaScript 및 CSS등 소스 축소

    개요

    분류: Javascript, CSS

    축소(Minification)는 코드에서 불필요한 문자를 제거하여 크기를 줄이고 로드 시간을 개선하는 방법이다.

    코드가 축소되면 불필요한 공백문자 (공백, 줄 바꿈 및 탭)뿐만 아니라 모든 주석이 제거된다.

    JavaScript의 경우 다운로드 한 파일의 크기가 줄어들기  때문에 응답 시간 성능이 향상된다.

     

    난독화(Obfuscation)는 소스 코드에 적용 할 수 있는 또 다른 최적화. 

    미국의 상위 10 개 웹 사이트를 대상으로 한 설문 조사에 따르면 축소처리(21%)에 비해 난독처리(25%)의 효율이 좋았다고 한다. 

    맹글링(Mangling) 같은 기법을 사용해 최적화를 하는데 최소화보다 복잡하므로 난독화 단계 자체로 인해 버그가 발생할 가능성도 있다는 점은 고려대상.

    function_long_long_name을 fn1 처럼 만들어주어야 하니까.

     

    외부 스크립트와 스타일을 축소하는 것 외에도 인라인 <script> 및 <style> 블록 을 축소 할 수 있다.

    스크립트와 스타일을 압축해도 크기를 줄이면 크기가 5 % 이상 줄어든다.

    JavaScript 및 CSS의 사용 및 크기가 증가함에 따라 코드를 축소하여 비용을 절약 할 수 있다.

     

    해결방법

    Uglify vs. Babel-minify vs. Terser: A mini battle royale라는 글에 따르면 

    terser의 성능이 상당히 좋았다.

     

    CSS의 경우 CSS Minification Benchmark에 따르면 Crass나 CSS Nano가 좋아보인다.

    또한 안쓰이는 스타일을 제거하는

    를 추가적으로 사용할 수도 있다.

     

    Webpack용 플러그인으로

    가 유명하다.

     

    기타 Node.js에서 작동하는 압축기를 찾아보면

    정도가 좋은 것 같다.

     

    사이트를 이용해서 살펴볼 생각이라면 다음 링크가 유용하다.

    https://black7375.tumblr.com/post/180878742065/%EC%9B%B9%EC%86%8C%EC%8A%A4-%EC%95%95%EC%B6%95

    SCRIPT

    https://assets.tumblr.com/post.js

     

     

    1.1.5 웹폰트 압축 및 서브셋

    개요

    분류: Font

    웹폰트 관련 확장기능에 잠시 손을 댄적이 있다보니 관련글도 많이 읽어보고, 실제로 적용도 해봤었다 ㅎㅎ[웹 폰트 사용과 최적화의 최근 동향, 웹폰트 최적화, 웹폰트 사용하기 (웹폰트 101)]

    한글 폰트 기준, 대부분 용량이 1~2메가를 훌쩍 넘는일이 비일비재하다.

    심지어 Noto Sans CJK는 44,683자를 포함하여 15.7MB에 달한다.

     

    웹에서 1메가만 되도 엄청나게 커다란 용량이니 당연히, 줄이는 것이 필요하며 웹폰트는 CSSOM을 생성하는데 필요하므로 렌더링을 차단하는 현상이 일어난다.

     

    - Font Face

    @font-face를 통해 폰트 패밀리 명을 설정할 수 있다.

    이때 성능과 관계된 것 위주로 알아보도록 하자.

    @font-face {
      font-family: NanumSquareWeb;
      src: url(NanumSquareR.eot); /* IE 호환성 보기 */
      src: local(NanumSquareR),
           local(NanumSquare),
           url(NanumSquareR.eot?#iefix) format('embedded-opentype'), /* IE 6 ~ 8 */
           url(NanumSquareR.woff2) format('woff2'),
           url(NanumSquareR.woff) format('woff'),
           url(NanumSquareR.ttf) format('truetype'),
           url(NanumSquareR.svg#NanumSquareR) format('svg'); /* 구 모바일 브라우저 */
      unicode-range: U+0-10FFFF;
    }

     

     

    해결방안

    - src

    계속 겪을 일이지만 웹브라우저는 지원하는 형식 중 첫번째를 고른다는 것을 염두해두어야 한다.

    • src에서 local을 사용하면 컴퓨터에 설치된 글꼴로 로드하므로 빠르다.
    • url을 설정할 때 format을 사용하면 지원가능한 파일만 다운로드 받으므로 빠르다.

     

    - 포맷

    사용할 수 있는 웹폰트 포맷에는 eot(IE 전용), ttf, woff2, woff, svg가 있다.

    이중 SVG는 거의 사용되지 않으며, eot와 ttf는 기본적으로 압축되지 않는다.

    • eot와 ttf는 gzip과 같은 압축을 설정해야 한다.
    • Zopfli 압축을 통해 eot, ttf, woff를 5%정도 더 압축 가능
    • woff2의 압축률이 가장 높다(30% 이상)[WOFF 2.0 Evaluation Report]
    • 특정 플랫폼에서 필요하지 않은 힌트커닝같은 메타데이터에 대해서 최적화 할 수도

     

    - unicode-range 및 서브셋

    unicode-range를 이용하면 범위 구분값 목록을 지정할 수 있다.

    • 단일 코드 포인트: 단일 문자를 지정(예: U+416)
    • 간격 범위: 범위의 시작과 끝 코드 포인트 지정(예: U+400-4ff)
    • 와일드 카드 범위: ?문자를 이용해 임의의 16진수를 나타냄(예: U+4??)

     

    중요한 점은 각 unicode-range마다 폰트파일을 따로 생성해줘야 한다는것.

    /* NanumSquareR.woff 원본: 3MB */
    @font-face {
      font-family: NanumSquareWeb;
      src: url(NanumSquareR-Arab.woff) format('woff'); /* 나눔스퀘어 아랍어 부분: 15KB */
      unicode-range: U+06??; /* 아랍어 */
    }
    @font-face {
      font-family: NanumSquareWeb;
      src: url(NanumSquareR-Cambodia.woff) format('woff'); /* 나눔스퀘어 아랍어 부분: 7KB */
      unicode-range: U+1780-17FF; /* 캄보디아어 */
    }

     

    그럼 알파벳 26자보다 훨씬 크기가 큰 한글은 어떻게, 얼마나 나누는 것일까?

    원본 나눔고딕은 한글 11,172자를 포함한다.

    그러나 노란색 글씨처럼 자주 사용하지 않는 글자들이 있다.

     

    KS X 1001은 "정보 교환용 부호계 (한글 및 한자)"라고 하여 자주 쓰이는 글자들을 모아놓았다.

    이 중 한글은 2350자.

     

    그런데 워낙 옛날에 제정된 것이기도 하고, 약간의 확장을 원한다면 노민지, 윤민구. KS 코드 완성형 한글의 추가 제안(224자)를 참고해보아도 좋다. [총2754자]

     

    포맷과 subset 변환은 pyftsubset을 사용하면 된다. [fontsubset.sh 명령어]

    나눔고딕 기준으로 2.2MB에서 267KB까지 줄일 수 있었다.

     

     

    구글은 한발 더 나아간다. [Google Fonts + 한국어 소개, 구글, 한글 글꼴 공식 지원... 머신러닝으로 용량 문제 해결]

    Google Fonts는 머신 러닝에 기반을 둔 최적화 기술을 통해 한글 폰트를 동적으로 분할 다운로드합니다.
    웹상의 방대한 한국어 문서를 분석한 결과, Google은 주제에 따라 사용되는 글자의 패턴을 발견하고, 패턴에 따라 한글 폰트에 포함된 17,388개의 글리프를 100여 가지 그룹으로 나누었습니다. 여기에 Google Fonts는 사용자가 웹 페이지를 불러올 때, 폰트 전체를 다운로드 하는 대신 내용을 표시하는 데 꼭 필요한 몇 가지 그룹만을 선택적으로 다운로드 하는 방식으로 폰트를 제공합니다. 이 기술을 적용한 Google Font를 사용하면 보다 빠르게 폰트 전체를 다운로드한 것과 다름없는 페이지를 제공할 수 있습니다.
    또한 Google Fonts API의 사이트 간 캐싱(cross-site caching)을 통해 해당 폰트가 여러 웹사이트에서 사용될수록 전체 다운로드 시간은 줄어들고, 한글 웹 폰트를 둘러싼 사용자 경험은 그만큼 개선될 것입니다.

     

    역시 나눔고딕을 예로 들면

    /* [0] */
    @font-face {
      font-family: 'Nanum Gothic';
      font-style: normal;
      font-weight: 400;
      src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.0.woff2) format('woff2');
      unicode-range: U+f9ca-fa0b, U+ff03-ff05, U+ff07, U+ff0a-ff0b, U+ff0d-ff19, U+ff1b, U+ff1d, U+ff20-ff5b, U+ff5d, U+ffe0-ffe3, U+ffe5-ffe6;
    }
    /* [1] */
    @font-face {
      font-family: 'Nanum Gothic';
      font-style: normal;
      font-weight: 400;
      src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.1.woff2) format('woff2');
      unicode-range: U+f92f-f980, U+f982-f9c9;
    }
    /* [2] */
    @font-face {
      font-family: 'Nanum Gothic';
      font-style: normal;
      font-weight: 400;
      src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.2.woff2) format('woff2');
      unicode-range: U+d723-d728, U+d72a-d733, U+d735-d748, U+d74a-d74f, U+d752-d753, U+d755-d757, U+d75a-d75f, U+d762-d764, U+d766-d768, U+d76a-d76b, U+d76d-d76f, U+d771-d787, U+d789-d78b, U+d78d-d78f, U+d791-d797, U+d79a, U+d79c, U+d79e-d7a3, U+f900-f909, U+f90b-f92e;
    }
    ...
    /* [117] */
    @font-face {
      font-family: 'Nanum Gothic';
      font-style: normal;
      font-weight: 400;
      src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.117.woff2) format('woff2');
      unicode-range: U+d, U+48, U+7c, U+ac10, U+ac15, U+ac74, U+ac80, U+ac83, U+acc4, U+ad11, U+ad50, U+ad6d, U+adfc, U+ae00, U+ae08, U+ae4c, U+b0a8, U+b124, U+b144, U+b178, U+b274, U+b2a5, U+b2e8, U+b2f9, U+b354, U+b370, U+b418, U+b41c, U+b4f1, U+b514, U+b798, U+b808, U+b824-b825, U+b8cc, U+b978, U+b9d0, U+b9e4, U+baa9, U+bb3c, U+bc18, U+bc1c, U+bc30, U+bc84, U+bcf5, U+bcf8, U+bd84, U+be0c, U+be14, U+c0b0, U+c0c9, U+c0dd, U+c124, U+c2dd, U+c2e4, U+c2ec, U+c54c, U+c57c-c57d, U+c591, U+c5c5-c5c6, U+c5ed, U+c608, U+c640, U+c6b8, U+c6d4, U+c784, U+c7ac, U+c800-c801, U+c9c1, U+c9d1, U+cc28, U+cc98, U+cc9c, U+ccad, U+cd5c, U+cd94, U+cd9c, U+cde8, U+ce68, U+cf54, U+d0dc, U+d14c, U+d1a0, U+d1b5, U+d2f0, U+d30c, U+d310, U+d398, U+d45c, U+d50c, U+d53c, U+d560, U+d568, U+d589, U+d604, U+d6c4, U+d788;
    }
    /* [118] */
    @font-face {
      font-family: 'Nanum Gothic';
      font-style: normal;
      font-weight: 400;
      src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.118.woff2) format('woff2');
      unicode-range: U+39, U+49, U+4d-4e, U+a0, U+ac04, U+ac1c, U+ac70, U+ac8c, U+acbd, U+acf5, U+acfc, U+ad00, U+ad6c, U+adf8, U+b098, U+b0b4, U+b294, U+b2c8, U+b300, U+b3c4, U+b3d9, U+b4dc, U+b4e4, U+b77c, U+b7ec, U+b85d, U+b97c, U+b9c8, U+b9cc, U+ba54, U+ba74, U+ba85, U+baa8, U+bb34, U+bb38, U+bbf8, U+bc14, U+bc29, U+bc88, U+bcf4, U+bd80, U+be44, U+c0c1, U+c11c, U+c120, U+c131, U+c138, U+c18c, U+c218, U+c2b5, U+c2e0, U+c544, U+c548, U+c5b4, U+c5d0, U+c5ec, U+c5f0, U+c601, U+c624, U+c694, U+c6a9, U+c6b0, U+c6b4, U+c6d0, U+c704, U+c720, U+c73c, U+c740, U+c744, U+c74c, U+c758, U+c77c, U+c785, U+c788, U+c790-c791, U+c7a5, U+c804, U+c815, U+c81c, U+c870, U+c8fc, U+c911, U+c9c4, U+ccb4, U+ce58, U+ce74, U+d06c, U+d0c0, U+d130, U+d2b8, U+d3ec, U+d504, U+d55c, U+d569, U+d574, U+d638, U+d654, U+d68c;
    }
    /* [119] */
    @font-face {
      font-family: 'Nanum Gothic';
      font-style: normal;
      font-weight: 400;
      src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.119.woff2) format('woff2');
      unicode-range: U+20-22, U+27-2a, U+2c-38, U+3a-3b, U+3f, U+41-47, U+4a-4c, U+4f-5d, U+61-7b, U+7d, U+a1, U+ab, U+ae, U+b7, U+bb, U+bf, U+2013-2014, U+201c-201d, U+2122, U+ac00, U+ace0, U+ae30, U+b2e4, U+b85c, U+b9ac, U+c0ac, U+c2a4, U+c2dc, U+c774, U+c778, U+c9c0, U+d558;
    }

    처럼 이루어져 있다.

     

    CSS 제공또한 여러개를 하나로 묶어서 받을 수 있는 점은 요청수를 줄인다는 점에서 또 다른 강점.

    <link href="https://fonts.googleapis.com/css2?family=Nanum+Gothic&family=Nanum+Gothic+Coding:wght@400;700&display=swap" rel="stylesheet"> 

    위 코드는 나눔고딕 400, 나눔고딕 코딩 400과 700. 3개로 이루어져있다.

     

    1.1.6 이미지 압축

    개요

    분류: Image

     

    이미지가 웹에서 차지하는 용량은 매우 큰 편이다.

    따라서 소스를 줄였던 것처럼 이미지도 압축해보는 것을 고려할 수 있다.

     

    이미지 압축을 할때 가장 큰 원칙은 "괜찮게 보이는 가장 낮은 품질"로 압축하는 것이다.

    1. 모든 이미지에 대해 압축 품질을 가능한만큼 직접 튜닝하라.
    2. 나머지는 가장 높은 성능을 얻기 위해 최적화를 자동화하라.

     

    해결방법

    많이 쓰이는 포맷인 GIF, PNG, JPEG를 대상으로 아라보자.

    - GIF

    Gifsicle가 좋아보인다.

    지금은 Gifsicle에 포함된 프로젝트인 Giflossy의 설명에 따르면 다음과 같이 명령어를 실행하면 최적화 할 수 있다.

    gifsicle -O3 --lossy=80 -o RESULT.gif IMAGE.gif

     

    - PNG

    이미지 품질을 중요시 여기면 optipng, 용량을 중요시하면 pngquant가 좋다.[pngquant vs pngcrush vs optipng vs pngnq, Experimenting with PNG and JPG Optimization]

    일단 YDN에서 소개한 pngcrush는 다음과 같이 실행할 수 있다.

    pngcrush IMAGE.png -rem alla -reduce -brute RESULT.png

    pngquant는 손실 압축이었고, 무손실 압축을 찾는다면 Zopfli(방법1, 방법2)나 PNGOUT을 쓸 수 있다.

    Zopfili는 deflate(zip, gzip 등)와 호환성을 보장하는 압축 알고리즘이다.

     

    - JPEG

    JPEG에서는 MozJPEG가 좋다.[Comparison of JPEG Lossless Compression Tools mozjpeg vs libjpeg-turbo]

    역시 YDN의 소개에 나온 jpegtran은 다음처럼 실행가능하다.

    jpegtran -copy none -optimize -perfect IMAGE.jpg RESULT.jpg

    회전(rotation)과 같이 손실 없는 JPEG 작업을 수행하고 최적화를 위한 사용이 가능하며, 이미지들로부터의 다른 쓸모 없는 정보(EXIF 정보와 같은)와 코멘트(comments)를 제거할 수 있다고 한다.

    Guetzli라고 구글이 2017년에 발표한 인코더도 있는데 Google Guetzli vs MozJPEG: Why Google’s New JPEG Encoder Can’t Dethrone the Product Image King을 보면 품질은 좋으나 매우 느리다. 크기가 매우 크거나 작은 이미지가 아니라면 효율성이 낮기 때문에 쓰지 않는 것이 좋다. 단, 극도의 효율성을 위해서라면 크기가 큰 경우 MozJPEG을 사용한 후 같이 써도 좋은 모양. [Squeezing JPEG Images with Guetzli, Image Compression Benchmarks]

     

    JPEG의 경우 Progressive를 활성화시키는 것도 좋다.

    하지만 약간의 트레이드 오프가 있다는 것.

    장점

    • 더 빠르게 로딩된다고 느낌
    • 더 높은 압축효율(2-10%)

     

    단점

    • 느린 디코딩(최대 3배)
    • 매우 작은 이미지는 더 클 수도 있음
    • 완전히 로드되었는지 알기 힘듦

     

    - EXIF 제거

    또한 Exif를 제거하는 것도 좋은 생각이다.[What Is EXIF Data?]

     

     

    - NodeJS 바인딩

     

    1.1.7 이미지, 영상 포맷

    개요

    분류: Image, Video

    - 이미지

    사용자나 디자이너가 웹페이지에 대한 이미지 생성을 완료하면, 시도해 볼 수 있는 최적화 사항들이 몇 가지 더 있다.

    특히 비효율적인 것으로 유명한 GIF는 최적화의 여지가 크다.

    https://medium.com/vingle-tech-blog/stop-using-gif-as-animation-3c6d223fd35a

     

    GIF 사용을 멈춰주세요!

    IMO: Converting video to gif is a terrible hack of convenience

    medium.com

     

    이외 이미지 최적화와 관련된 내용은 다음 글에서 잘 설명해주고 있다.

    https://www.html5rocks.com/ko/tutorials/speed/img-compression/

     

    Image Compression for Web Developers - HTML5 Rocks

    Reduce your page size by looking under the hood of image compression.

    www.html5rocks.com

    https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types

     

    Image file type and format guide

    In this guide, we'll cover the image file types generally supported by web browsers, and provide insights that will help you select the most appropriate formats to use for your site's imagery.

    developer.mozilla.org

     

    - 영상

    https://developer.mozilla.org/ko/docs/Web/Media/Formats/%EB%B9%84%EB%94%94%EC%98%A4%EC%BD%94%EB%8D%B1

     

    웹 비디오 코덱 가이드

    이 문서는 웹에서 흔히 볼 수 있는 비디오 코덱을 소개하고 각각의 기능과 호환성, 사용성에 대해 설명하고 여러분에게 필요한 적절한 코덱을 찾는 법을 안내합니다. MPEG-2 Part 2 ITU가 설계한 H.262를 참조하여 MPEG-2 스펙에서 정의한 비디오 포맷이며입니다.

    developer.mozilla.org

    - 음성

    https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Audio_codecs

     

    Web audio codec guide

    In this article, we look at audio codecs used on the web to compress and decompress audio, what their capabilities and use cases are, and offer guidance when choosing audio codecs to use for your content.

    developer.mozilla.org

     

    측정방법

    GIF의 경우 이미지 색상들의 숫자에 상응하는 팔레트 크기를 사용하는지 ImageMagick을 사용하면 확인할 수 있다.

    identify -verbose IMAGE.gif 

     

    팔레트에서 4가지 색상과 256 색상 슬롯(slot)을 사용하는 이미지를 보면 개선의 여지가 있다

     

     

    해결방법

    - GIF

    정적인 GIF 파일은 PNG로 바꾸어보자.(ImageMagick 기준)

    convert IMAGE.gif IMAGE.png

     

    GIF 최적화를 위해 앞서 나온 Gifsicle같은 것을 이용할 수도 있다지만..

    동적인 GIF 파일이라면 MP4처럼 영상파일로 바꾸는 것이 좋다.(ffmpeg 사용, 명령어 참고 )

    ffmpeg -f gif -i ANIMATE_IMAGE.gif -pix_fmt yuv420p -c:v libx264 -movflags +faststart -filter:v crop='floor(in_w/2)*2:floor(in_h/2)*2' ANIMATE_IMAGE.mp4

    GIF는 영상 재생을 위해 나온 포맷이 아니다보니 미디엄 포스트에서 나온 것처럼 10배 가까이 용량차이가 생기기도 한다.

    실제로  Gfycat, Giphy처럼 GIF를 전문적으로 다루는 사이트나 짤로서 GIF가 많이 이용되는 사이트는 트래픽을 줄이고, 속도를 높이기 위해 영상으로 변환한 것을 자주 볼 수 있다.

     

    영상으로 바꾸어도 HTML 마크업은 크게 복잡해지지 않는다.

    단, 자동재생(autoplay), 반복(loop), 소리가 없음(muted)란 gif의 특징은 따라야할 필요가 있다.

    <!--  OLD GIF Markup  -->
    <img src="https://cdn.my-awesome-website.com/images/awesome-animation.gif" alt="My awesome animated image">
    
    
    <!--  NEW Video Markup  -->
    <video autoplay loop muted playsinline>
      <!--  Specify video sources  -->
      <source src="https://cdn.my-awesome-website.com/images/awesome-and-efficient-animation.webm" type="video/webm">
      <source src="https://cdn.my-awesome-website.com/images/awesome-and-efficient-animation.mp4" type="video/mp4">
      
      <!--  FALLBACK for legacy browsers!  -->
      <img src="https://cdn.my-awesome-website.com/images/awesome-animation.gif" alt="My awesome animated image">
    </video>

    MP4(H.264) 대신 WebM을 써도 좋다.

    WebM은 효율적인 비디오(VP8, VP9, AV1)와 오디오(Vorbis, Opus) 코덱들을 지원한다.

     

    - WebP

    JPEG보다 평균 25–35%, PNG보다 26% 크기가 작을정도로 효율적인 이미지 포맷입니다.

    많은 브라우저에서 지원하므로 충분히 시도해볼만 합니다.

     

    WebP를 지원하지 않는 브라우저가(대표적으로 사파리) 존재하기 때문에 생기는 문제는 <picture>태그를 사용하면 된다.

    source중 첫번째로 매칭되는 것(지원하는 것)을 가져오므로 최적화가 된 것을 앞에 두어야 한다.[Selecting an image source]

    <picture>
      <source srcset="/path/to/image.webp" type="image/webp">
      <img src="/path/to/image.jpg" alt="">
    </picture>
    
    <picture>
       <source srcset="photo.jxr" type="image/vnd.ms-photo">
       <source srcset="photo.jp2" type="image/jp2">
       <source srcset="photo.webp" type="image/webp">
       <img src="photo.jpg" alt="My beautiful face">
    </picture>

     

    - SVG나 CSS

    간단한 이미지를 SVG나 CSS를 사용하면 텍스트로 만들어지기 때문에 큰 폭으로 이미지 크기를 줄일 수 있다.[A Beginner’s Guide to Pure CSS Images, 4 Ways for Using CSS Instead of Images for Design Effects]

    특히 CSS는 메인 쓰레드가 아니라 컴포지터 쓰레드를 사용하므로 성능에 좋다.

     

    다음은 SVG와 CSS를 비교한 어도비의 문서들이다.

     

    - 기타포멧

    기타 효율적이지만 지원하지 않거나 앞으로 지원가능성이 있는 이미지 포맷들의 리스트다.

    개인적으로 AVIF에 기대를 걸고 있다.

    • FLIF: 압축률 기준으로 PNG, WebP, BPG등보다 좋다고 주장하는 무손실 포맷 [Poly-Fill]
    • HEIF:  아래나올 HEVC를 이용한 이미지 포맷, 애플이 사용하는 것으로 유명
    • AVIF: 역시 아래나올 AV1을 이용한 이미지 포맷

     

    - 영상코덱

    아직은 하드웨어 가속이 떨어지기 때문에 H.264를 이용하는 것이 낫겠지만 몇년후 지원기기가 많아지면 효율이 좋은 AV1을 사용해보는 것도 괜찮겠다. AV1 구현중 SVT-AV1 ffmpeg(플러그인)에 붙여쓰는 형태가 되지 않을까 싶다. [AV1 vs HEVC: Should You Abandon HEVC Now?, AV1 is ready for prime time: SVT-AV1 beats x265 and libvpx in quality, bitrate and speed, Reddit Benchmark1, Reddit Benchmark2]

     

     

    - NodeJS 바인딩

     

    이것저것을 찾다보니 imagemin이란 것을 알게 되었는데 여러 이미지 최적화를 한꺼번에 관리하기 편할 것 같다.

     

    1.1.8 HTML에서 이미지 크기를 조정하지 않기

    개요

    분류: Image

    - 리사이징

    HTML에서 너비와 높이를 설정할 수 있다고 필요보다 큰 이미지를 사용하지 말자는 것. 

    <img width="100" height="100" src="IMAGE.jpg" alt="IMAGE" />

    만약 위와 같은 구문을 원한다면 이미지 (IMAGE.jpg)는 축소된 500x500px 이미지가 아니라 100x100px이어야 한다.

     

    커다란 이미지는

    • 커다란 용량으로 다운로드 시간 낭비
    • 대역폭을 낭비로 HTTP 연결갯수에 제한이 생길 가능성
    • 메모리 크기
    • 디코딩 시 비용
    • 리스케일시 비용과 이미지 품질 미보장

    처럼 많은 악영향을 끼친다.

     

    - 반응형

    크기에 꼭 맞게 하라는 말은 반응형 웹의 개념이 필요하다는 뜻이다.

    즉, 각 해상도에 맞는 이미지를 제공해야 한다.[MDN 반응형 이미지구글 반응형 이미지]

    이때 해상도마다 잘라내기나 확대가 다른 아트디렉션도 고려해야 한다. [Response Images Demo]

     

    해결방법

    - 변환

    비트윈은 Skia 이용해 리사이징을 했지만 Sharp란 패키지도 좋은 것 같다.

    실제로 Skia의 성능은 매우 우수해 카이로를 제치고 웹브라우저에서 2D 그래픽용으로 사용되고 있다.

     

    - 반응형

    해상도

    각 해상도에 맞는 이미지를 사용하려면 srcset 속성을 사용하면 된다.

    <img srcset="EXAMPLE-320w.jpg,
                 EXAMPLE-480w.jpg 1.5x,
                 EXAMPLE-640w.jpg 2x"
         src="EXAMPLE-640w.jpg" alt="Example Image!!">

    물론 앞서 나온 <picture>태그처럼 srcset을 지원하지 않으면 기본값인 src를 사용한다.

     

    크기

    sizes 속성으로 화면 크기에 따라 다른 크기의 이미지를 로드할 수도 있다.

    <img srcset="EXAMPLE-320w.jpg 320w,
                 EXAMPLE-480w.jpg 480w,
                 EXAMPLE-800w.jpg 800w"
         sizes="(max-width: 320px) 280px,
                (max-width: 480px) 440px,
                800px"
         src="EXAMPLE-800w.jpg" alt="Example Image!!">

     

    아트디렉션

    아트디렉션을 사용하기 위해서는 <picture> 태그가 필요하다.

    source에서 media 속성을 사용해 어떠한 이미지를 보여줄지 결정한다.

    미디어 속성은 추후에 등장할 미디어 쿼리에서 더 설명한다.

    <picture>
      <source media="(max-width: 799px)" srcset="EXAMPLE-480w-close-portrait.jpg">
      <source media="(min-width: 800px)" srcset="EXAMPLE-800w.jpg">
      <img src="EXAMPLE-800w.jpg" alt="Example Image!!">
    </picture>

     

    1.1.9 파비콘을 작고 캐시 가능하게 만들기

    개요

    분류: Image

    파비콘(favicon)은 주소창에 표시되는 아이콘으로, 주로 서버의 루트에 위치한다.(파비콘(Favicon)의 모든 것)

    신경 쓰지 않아도 브라우저가 계속 요청하므로으로 404 Not Found가 아닌 것이 좋다.

    또한 동일한 서버에 있기 때문에 쿠키는 요청 될 때마다 전송되며 다운로드 순서를 방해하기도 한다.

    (예를 들어, 온로드에서 추가 구성 요소(component)를 요청하면 IE에서 이러한 추가 구성 요소보다 파비콘이 다운로드됩니다).

     

    해결방법

    파비콘의 단점을 보완하기 위해서는 다음을 지키는 것이 좋다.

    • 1k 이하로 작게 만들기
    • 변경설정 후에는 이름변경이 불가능하므로 원하는 시점의 Expires header를 설정.
      아마 미래 시점(약 몇 달 뒤)의 Expires header를 안전하게 설정할 수 있을 것이다. 명확한 결정을 위해 현재 파비콘의 마지막 수정날짜 확인이 가능하다.

    Favicon Generator를 이용해서 생성하면 다양한 기기에 대응할 수 있다.

     

    1.1.10 HTTP 구성요소 압축

    개요

    분류: Server

    HTTP 응답 구성요소를 압축하여 줄임으로써 응답 시간을 줄일 수 있다.[Content Negotiation]

     

     

    웹 클라이언트는 HTTP 요청에서 Accept-Encoding 헤더를 사용하여 지원하는 압축을 나타낸다.

          Accept-Encoding: br, gzip, deflate


    웹 서버가 요청에서 이 헤더를 발견하면 클라이언트가 나열한 방법 중 하나를 사용하여 응답을 압축 할 수 있다.

    웹 서버는 응답의 Content-Encoding 헤더를 통해 웹 클라이언트에게 이를 알린다.

          Content-Encoding: gzip

     

    서버는 파일유형에 따라 압축대상을 선택할 수 있지만 일반적으로 이 결정에는 많은 제약이 따른다.

    대부분의 웹사이트들은 자신들의 HTML 문서를 압축한다. 그 압축툴은 scripts와 stylesheets를 압축하는 데에도 매우 유용한 것들이지만 많은 웹사이트들은 이 기회를 놓치고 있다. 이미지와 PDF 파일은 이미 압축이 되어있기 때문에 압축할 필요가 없지만 XML이나 JSON을 포함한 텍스트 응답을 압축하는 데에는 유용하다. CPU를 낭비할 뿐 아니라 잠재적으로 파일크기를 증가시킬 수 있기 때문이다.

     

    가능한 많은 파일 형식을 압축하는 것이 페이지 무게를 줄이고 사용자 경험(UX)을 가속화하는 쉬운 방법입니다.

     

    해결방안

    가장 좋은 것은 구글의 Brotli를 적용하는 것이다.[Introducing Support for Brotli Compression, Boosting Site Speed Using Brotli Compression]

    최신 브라우저들은 브로틀리를 지원하니 안심하고 써도 된다.

     

    • 아파치와 엔진엑스 설정법 [Apache: mode_brotli,  NGINX: brotli]
    • 웹팩(Webpack)에서 Brotli를 설정하고, Gzip을 Fallback으로 두는 방법
    • 서버가 php 사용시 다음과 같이 압축된 것을 요구 가능.
    <?php if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) ob_start("ob_gzhandler"); else ob_start(); ?>

     

    Gzip에는 앞서 언급한 Zopfli를 적용해볼 수 있다.(단, 시간이 약간 더 필요하다는 것은 고려대상)

     

    lz4도 성능이 엄청 좋은데..(인코딩, 디코딩, 압축률 모두에서.. 개인 컴퓨터 커널 압축에 써먹는중)

    왜 언급이 안되나 궁금.

     

    1.1.11 클라이언트 힌트로 리소스 선택 자동화

    개요

    분류: Server

    클라이언트 힌트를 사용하여 리소스 선택을 자동화 할 수 있다.[Adapting to Users with Client Hints, Automating Resource Selection with Client Hints, Client Hints]

     

    앞선 최적화의 영향으로 우리는 최적화된 이미지 포맷(WebP, JPEG XR, JPEG), 해상도(1x, 1.5x, 2x, 2.5x, 3x), 뷰포트 너비에 따라 제어를 하고 싶어했고 그 결과 아래처럼 거대한 <picture> 태그가 완성된다.

    <picture>
      <!-- serve WebP to Chrome and Opera -->
      <source
        media="(min-width: 50em)"
        sizes="50vw"
        srcset="/image/thing-200.webp 200w, /image/thing-400.webp 400w,
            /image/thing-800.webp 800w, /image/thing-1200.webp 1200w,
            /image/thing-1600.webp 1600w, /image/thing-2000.webp 2000w"
        type="image/webp">
      <source
        sizes="(min-width: 30em) 100vw"
        srcset="/image/thing-crop-200.webp 200w, /image/thing-crop-400.webp 400w,
            /image/thing-crop-800.webp 800w, /image/thing-crop-1200.webp 1200w,
            /image/thing-crop-1600.webp 1600w, /image/thing-crop-2000.webp 2000w"
        type="image/webp">
      <!-- serve JPEG to others -->
      <source
        media="(min-width: 50em)"
        sizes="50vw"
        srcset="/image/thing-200.jpg 200w, /image/thing-400.jpg 400w,
            /image/thing-800.jpg 800w, /image/thing-1200.jpg 1200w,
            /image/thing-1600.jpg 1600w, /image/thing-2000.jpg 2000w">
      <source
        sizes="(min-width: 30em) 100vw"
        srcset="/image/thing-crop-200.jpg 200w, /image/thing-crop-400.jpg 400w,
            /image/thing-crop-800.jpg 800w, /image/thing-crop-1200.jpg 1200w,
            /image/thing-crop-1600.jpg 1600w, /image/thing-crop-2000.jpg 2000w">
      <!-- fallback for browsers that don't support picture -->
      <img src="/image/thing.jpg" width="50%">
    </picture>

     

    클라이언트 힌트는 서버에서 고르도록해 매우 간단하게 만들 수 있다.

     

    - 용어 설명

    클라이언트 힌트에서 나오는 용어들을 설명한다.[CSS 에서 항목 크기 조정]

     

    • 고유 크기(Intrinsic Size): CSS등의 영향을 받기 전에 설정된 고유의 크기
    • 외적인 크기(Extrinsic Size): CSS등을 사용해 설정한 특정 크기

     

    • 밀도가 수정된 고유 크기(Density-corrected intrinsic size): 고유 크기를 장치 픽셀 비율로 나눈 값, 픽셀 밀도에 맞춰 수정된 미디어 리소스의 크기.
    <img src="whats-up-1x.png"
         srcset="whats-up-2x.png 2x, whats-up-1x.png 1x"
         alt="I'm that image you wanted.">

    고유 크기가 1x지만, 화면의 픽셀 비율이 2x인 장치를 사용하면 2x 이미지가 요청되는 식.

     

     

    • Viewport: 브라우저의 페이지에서 가시영역. [Viewport meta 태그를 이용해 모바일 브라우저상에서 레이아웃 조종하는 법]
      • 데스크톱 브라우저의 창크기, 모바일 브라우저의 줌크기에 따라 달라진다.
      • viewport의 width 값에 따라 아트디렉션도 고를 수 있다.
    • DPR(Device Pixel Ratio): 사용자 화면의 물리적 픽셀과 CSS 픽셀의 비율 [devicePixelRatio]
    • Width: 이미지 리소스 요청에 대한 힌트가 나타남(예: DPR이 2, CSS의 너비가 320,  뷰포트 너비가 85라면 $(2 \times 320) \times 0.85$
    • Content-DPR: 리소스 자체 픽셀과 CSS 픽셀의 비율, DPR과 Width 헤더를 모두 사용할 때 외적인 크기로 달라질때 유용

     

     

    다음은 네트워크 정보들의 예.

    • RTT:  RTT 시간(ms)을 대략적(25ms 반올림)으로 제공
    • Downlink: 대역폭의 크기(Mbps)를 대략적(2kb)으로 제공
    • ECT(Effective Connection Types): RTT와 Downlink를 분석해 2g, 3g, 4g등으로 나눔
    • Save-Data: 적은 양의 데이터를 보내야 한다는 환경설정

     

     

    해결방법

    - Accept 헤더

    Accept헤더를 이용하면 브라우저가 컨텐츠 유형을 고를 수 있다.

    Accept: image/webp,image/apng,image/*,*/*;q=0.8

     

    PHP를 사용하면 이를 설정할 수 있다.

    <?php
    // Check Accept for an "image/webp" substring.
    $webp = stristr($_SERVER["HTTP_ACCEPT"], "image/webp") !== false ? true : false;
    
    // Set the image URL based on the browser's WebP support status.
    $imageFile = $webp ? "whats-up.webp" : "whats-up.jpg";
    ?>
    <img src="<?php echo($imageFile); ?>" alt="I'm an image!">

     

    - Accept-CH

    클라이언트 힌트는 DPR(Device Pixel Rate), Viewport-Width, Width를 서버에 알리도록 한다.

    <meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">
    ...
    <picture>
      <source media="(min-width: 50em)" sizes="50vw" srcset="/image/thing">
      <img sizes="100vw" src="/image/thing-crop">
    </picture>

     

    앞선 <picture> 태그보다 훨씬 간단하며 서버나 CDN을 통해 자동적으로 이미지를 다운받도록 만들 수 있다.

     

    네트워크

    네트워크의 경우 Save-Data가 on인지를 가장 먼저 확인하고, 없다면 네트워크 점수(0~1 사이)를 내서 사용할 수 있다.

    웹폰트, 이미지 회전 메뉴와 아코디언 동작 등을 위한 Javascript, 컨텐츠 이미지등을 생략시키는 예.

     

    다음과 같이 Save-Data가 활성화 되었는지 확인할 수 있다. 

    if ("connection" in navigator) {
        if (navigator.connection.saveData === true) {
            // Implement data saving operations here.
        }
    }

     

    • 필수적이지 않은 웹폰트 제거
    • 이미지를 낮은 해상도로
    • 경량화된 코드
    • 불필요한 코드 삭제
    • 검색시 적은 수의 결과를 반환하거나 미디어가 많은 결과를 제한

    기타 코드는 Delivering Fast and Light Applications with Save-Data를 참고바란다.

     

    1.1.12 구성 요소를 25K 미만으로 유지

    개요

    분류: Mobile
    이 제한은 iPhone이 25K보다 큰 구성 요소를 캐시하지 않는다는 사실과 관련이 있습니다.

    이것은 압축되지 않은 크기입니다.

     

    gzip만으로는 충분하지 않을 수 있으므로 최소화가 중요한 곳입니다.

    자세한 내용 은 Wayne Shea 및 Tenni Theurer의 " Performance Research, Part 5 : iPhone Cacheability-Making it Stick "을 확인하십시오.

     

    1.1.13 기타 번들러 최적화

    개요

    분류: Content, Javascript, CSS

    웹팩의 최적화를 다룬다.[webpack for real tasks: decreasing front-end size and improving caching, Web Performance Optimization with Webpack, Awesome Webpack Perf]

     

     

    1.2 요청 줄이기

    각종 리소스는 서버에 요청을 하고 나서야 다운로드 할 수 있기 때문에 요청을 줄이거나 요청 후 응답시간을 줄이는 것은 다운로드 시간을 줄이는데 도움이 된다.

    HTTP 1.1의 경우, 한 호스트에서 최대 6개의 리소스만 병렬적으로 가져올 수 있고, 그 이상은 대기한다.[Match Firefox's per-host connection limit of 15]

     

    1.2.1 DNS 조회 감소

    개요

    분류: Content

    DNS (Domain Name System)는 우리가 익히 알다시피 호스트 이름을 IP 주소에 매핑한다.

    DNS 조회에는 시간(약 20~120ms)이 드는데 DNS 조회가 완료 될 때까지 브라우저는 이 호스트 이름에서 아무것도 다운로드 할 수 없다.

     

    그래서 성능 향상을 위해 DNS 조회 정보가 캐싱된다.

    사용자의 ISP와 LAN에 의해 유지 관리되는 캐싱 서버말고도 개별 컴퓨터의 운영체제와 브라우저에서도 일어난다.

    클라이언트의 DNS 캐시가 비어있는 경우(브라우저와 운영 체제 모두) DNS 조회 수는 웹 페이지의 고유 호스트 이름 수와 같다.

    블로그 정보와 글: 티스토리, 이미지: imgur, react: jsDelivr, 부트스트랩: 클라우드 플레어, 폰트: 구글이라면 총 5개의 호스트가 되는 것이다.

    즉, 페이지 URL에 사용 된 호스트 이름, 이미지, 스크립트 파일, 스타일 시트등이 포함되며 고유 한 호스트 이름 수를 줄이면 DNS 조회 수가 줄어 들 수 있다는 것이다.


    하지만 고유 호스트 이름 수를 줄이면 병렬 다운로드가 줄어들 수 있다.(각 서버에서 병렬 다운로드에 제한을 걸 수 있음)

    따라서 DNS 조회가 줄었더라도 병렬 다운로드가 이루어지지 않아 응답 시간이 늘어날 수 있다.

     

    해결방법

    호스트 구성 요소를 2~4개로 적절히 분리통합하고 유지하는 것이 좋다.

     

    1.2.2 CDN 사용

    개요

    분류: Server

    웹 서버에 대한 사용자의 근접성은 응답 시간에 영향을 준다. 

    지리적으로 분산 된 여러 서버에 콘텐츠를 배포하면 사용자 관점에서 페이지를 더 빠르게 로드 할 수 있습니다. 

     

    해결방법

    이미 잘 분산되어있는 CDN 서비스를 이용하는 것이 좋다.

     

    https://www.cdnperf.com/

     

    CDNPerf - CDN Performance and Uptime monitoring, comparison and analytics - RUM data

    With CDNPerf you can find the fastest CDN provider in the world or just your country. Get the top CDN by using our data for free.

    www.cdnperf.com

     

    무료에서는 클라우드 플레어의 CDNjsjsDelivr, 구글(주요 라이브러리만 무료)이 유명하고 쓸만한편.

     

     

    1.2.3 404 피하기

    개요

    분류: Content
    HTTP 요청은 비용이 많이 들기 때문에 HTTP 요청을 하고 쓸모없는 응답 (예 : 404 찾을 수 없음)을 얻는 것은 완전히 불필요하며 아무런 이점없이 사용자 경험을 느리게 합니다.

    일부 사이트는 404때 "X를 의미 했습니까?"처럼 유용한 정보를 제공합니다. 이는 사용자 경험에는 좋지만 서버 리소스 (예 : 데이터베이스 등)를 낭비합니다.

     

    외부 JavaScript에 대한 링크가 잘못되고 결과가 404 인 경우 특히 나쁩니다.

    먼저 이 다운로드는 병렬 다운로드를 차단하며 브라우저는 JavaScript 코드인 것처럼 404 응답 본문(body)을 구문 분석하여 사용할 수있는 것을 찾으려고 시도 할 수 있습니다.

     

    측정방법

    broken-link-checker같은 NodeJS 패키지나 LinkChecker, Xenu’s link Sleuth와 같은 프로그램을 쓸 수 있다.

    사이트로는 Sitechecker, Dead Link Checker, W3C Linkchecker와 같은 것이 있다.

     

    1.2.4 서버 응답 시간 개선

    개요

    분류: Server

    서버 응답시간이 빠르면 좋으며 일반적으로 200 ms 아래면 좋다고 한다.

     

    느린 애플리케이션 로직, 느린 데이터베이스 쿼리, 느린 라우팅, 프레임워크, 라이브러리, 리소스 CPU 부족 현상 또는 메모리 부족 현상등이 원인일 수 있다.

    또한 서버의 응답시간의 변동폭은 적으므로 변동이 크다면 문제가 있다는 증거이기도 하다.

     

    해결방법

    크게 다음처럼 걸러낼 수 있다. [5 Ways to Reduce Server Response Times (TTFB)]

    • 트래픽처리를 위한 충분한 리소스
    • 아파치나 Nginx등 웹서버를 신중히 선택
    • 웹서버 최적화
    • WordPress나 Magento같은 CMS(Content Management System) 사용시 신중히 관리
    • DB 최적화

     

    여긴 프론트엔드의 영역이 아니니 더 자세한 설명은 생략한다.

     

    1.2.5 기타 네트워크 최적화

    개요

    분류: Server

    High Performance Browser Networking이라는 사이트의 내용이 전반적으로 너무 좋다.

    아래는 최적화 방법을 정리해 놓은 것.

    아마 최신 리눅스 커널들을 사용한다면 대부분 활성화 되어있을 것이다.

     

    - Transport & Session

    OSI 7 Layer에서 Transport 계층에 속하는 TCP와 Session에 속하는 TLS에 대해 다루어본다.

    최근 QUIC를 비롯해 제약이 덜한 UDP를 적극적으로 활용하는 경우가 많이 생기고 있다지만, 제약이 덜하다보니 튜닝 규칙또한 적기가 애매하여 제외하였다.

    일반기업들은 TCP를 주로 사용하며, UDP에서는 QUIC를 제외하면 적용할 일이 거의 없을만 하기도 하고.

     

    Session계층은 Transport보다 Application쪽과 가깝긴 한데..

    Application에서 HTTP를 중점적으로 다룰 것이며, RTT 개선의 측면에서 Transport와 같이 설명하기 편하므로 묶어서 설명한다.

     

    TCP

    TCP 성능 튜닝에 대한 이야기. [Building Blocks of TCP, TCP Tuning for HTTP]

    오랜만에 네트워크 책을 보는 느낌이 나는 듯 ㅎㅎ

    재밌어서 그림들도 끼워넣으며 설명해본다.

     

    먼저 혼잡제어에 대해 다루어보자. [사이 좋게 네트워크를 나눠 쓰는 방법, TCP의 혼잡 제어]

    • 초기 혼잡 윈도우(Congestion Window, CWND) 크기 증가

    초기 CWND가 크면(약 10 세그먼트) 첫번째 RTT에서 가져갈 수 있는 데이터양이 커지고, 혼잡제어 과정에서 윈도우의 크기를 빠르게 증가하는 것이 가능하다.

     

    • Slow-Start 재시작 비활성화

    Congestion Control

    일정 유휴시간(Idle)이 생기면 혼잡 윈도우 값을 초기화하여 Slow Start로 재시작 하는 상황이 생긴다.

    Slow-Start 재시작을 비활성화 하면 오래 지속되는 TCP 연결의 성능이 향상된다.

     

    • 윈도우 스케일링

    TCP의 RWND(수신 윈도우) 크기를 알리기 위해 16비트가 할당되어 있다.

    당연히 최대 크기는 $2^{16} = 65,536 \, byte$다.

    최대 RWND의 크기를 키우기 위해 윈도우 스케일링(RFC 1323)이 제공되서 1기가까지 키울 수 있고, 대역폭을 최대한 이용할 수 있다.

    원리는 기존 RWND값에 좌측으로 시프트할 갯수를 전달하는 것.

     

    • TCP Fast Open

    TCP Fast OPEN – Citrix NetScaler

    SYN 패킷에 데이터 전송을 허가해서 대기시간을 제거할 수 있다.

    구글에 따르면 HTTP 트랜젝션 네트워크 대기시간을 15%, 전체 페이지로드는 10~40%까지도 단축 시킬 수 있다고.

    관심이 생긴다면 다음문서에 있는 링크들을 확인 바란다. [TCP Fast Open - 보다 빠르게 웹 컨텐츠를 전송하기 위한 기술]

     

    2016년쯤에 엣지에 들어간다는 기사를 보고 알았던 것을 생각하면 비교적 최신 기술. [Enable TCP Fast Open in Microsoft Edge for quicker page load times]

     

    UDP

    관심이 있다면 다음을 읽어보자. [Building Blocks of UDP]

     

    TLS

    해당 링크에는 여러가지가 나왔지만 간단하게 3~4개만 다루려한다. [Transport Layer Security (TLS)]

    링크에는 대칭키 암호화 사용(공개키 대신), TLS 레코드 크기 최적화, 인증서 체인 최적화, OCSP(Online Certifiate Status Protocol) 스태플링 설정, HSTS(HTTP Strict Transport Security) 사용 등등이 등장한다.

     

    • 연결 재사용 / 구성 세션 캐싱

    TCP + TLS 연결 설정을 위한 대기시간과 계산 시간 오버헤드를 막기위해 연결 시간 종료값을 설정한다.

    SSL2.0에는 TLS 세션 캐싱이 도입되어 더욱 효율적으로 오버헤드를 제거할 수 있다.

     

    • CDN 활용

    CDN을 활용하면 물리적인 거리를 줄일 수 있기 때문에 유리하다.

     

    • TLS 1.3 활성화

    0-RTT로 오버헤드가 제거되었다. [알아두면 쓸데없는 신비한 TLS 1.3, Introducing Zero Round Trip Time Resumption (0-RTT)]

    Encryption Week

     

    +.

    현재 사용되지 않는 TLS False Start가 TLS 1.3의 아이디어와 살짝 비슷하다. [So long False Start, we hardly knew ye]

    ClientKeyExchange를 보내면 이미 암호화키를 알고 있는 것과 다름 없으므로 어플리케이션 데이터를 전송해도 무방하다.

     

    - Application

    HTTP 프로토콜의 최적화를 다루어보고자 한다.

    가장 좋은 것는 1.X보다 2를 3가 보급된다면 3를 사용하는 것이다.

     

    HTTP 1.X

    Keep-Alive, 파이프 라이닝을 다루었다. [HTTP/1.X]

    도메인 샤딩, 이미지 스프라이트와 같은 방법은 프론트엔드에서 최적화 가능한 부분이라 따로 분리했다.

     

    • Keep-Alive

    매번 연결을 다시 하는 것보다 연결을 유지하여 동일한 TCP 연결을 하용하자는 아이디어.

     

    • HTTP 파이프 라이닝

    파이프라이닝을 활용하면 Keep-Alive를 사용해 응답을 받지 않고도 여러개의 요청을 하는 것이 가능하다. [HTTP/1.x의 커넥션 관리]

     

     

    서버에서 FIFO로 각각이 아니라 병렬처리를 해서 합쳐보내줄 경우 더 단축시키는 것도 가능하다.

     

    HTTP 2

    구글의 SPDY에 기반해 만들어진 표준이다. [HTTP/2, HTTP/2 소개]

    • 헤더 압축 / 바이너리 프레임

    HPACK이라는 방식으로 헤더를 압축하여 전송한다.

    (정적 허프만 코드로 인코딩, 헤더 필드를 인덱싱화하여 관리하면서 업데이트)

     

    기존 텍스트방식에 비해 바이너리로 만들어져 파싱이 빠르고 요류도 줄어든다.

     

    • 멀티플렉싱

    HTTP 1.1의 파이프라이닝을 넘어서 다수의 응답과 요청을 동시적으로, 독립적으로 처리할 수 있다. [LoadMaster - HTTP/2]

     

    • 스트림 우선순위

    리소스 간 우선순위를 정하여 Critial Rendering Path의 개선에 도움이 된다.

     

    스트림은 위와 같이 이루어져 있다.

     

    • 서버푸시

    클라이언트의 요청이 없더라도 서버에서 컨텐츠를 Push할 수 있다. [HTTP/2 Server Push Tutorial]

     

     

    HTTP3(실험적, 개발중)

    HTTP3는 앞서 소개한 TLS 1.3과 UDP기반 프로토콜인 QUIC로 구성되어 있다. [HTTP/3: 과거, 현재 그리고 미래, HTTP/3 explained, 5G 초연결시대에 웹 HTTP의 대안은 QUIC?]

     

    • QUIC

    [The Road to QUIC]

    TCP는 신뢰성을 보장하기 위해 패킷이 손실되면 패킷을 다시 전송하고 목적지를 찾는 동안 전체 TCP 연결이 중단된다.

     하지만 QUIC는 UDP 기반이기 때문에 다른 스트림은 독립적으로 이루어지므로 모든 스트림의 전속을 막지 않는다.

     

     

    TCP Fasct Open과 TLS에서 있던 문제점을 수정, TLS 1.3으로 모두 암호화 되어 있다는 점이 특이하다.

     

    - 기타

    이 가이드의 또 다른 재미는 와이파이와 모바일에 대해 다루었다는 점이다.

     

    배터리등을 고려하는 것도 있지만 가장 눈에 띄는 것은 Adaptive HTTP Streaming이었다. [웹 기술로 구현하는 Adaptive HTTP Streaming]

     

    간단히 설명하자면, 여러가지 해상도로 세그먼트를 쪼개 저장해두고 네트워크 상태에 따라 좋은 것과 나쁜것을 적응형으로 골라 스트리밍하도록 한다는 이야기.

    넷플릭스와 같은 곳에서 적극적으로 사용하는 것으로 알고 있다.

     

    1.3 합치기

    대부분 요청을 줄이는 것이 핵심이라 어떻게 보면 줄이기의 연장선상이다.

     

    1.3.1 파일 결합하기

    개요

    분류: Content

    script와 css파일을 합쳐서 요청횟수를 줄이는 것.

    스크립트와 스타일 시트가 페이지마다 다를 경우 파일을 결합하는 것이 어렵겠지만 해결한다면 응답시간이 향상될 것이다.

     

    해결방법

    이를 가능하게 하는 대표적인 것이 패키지 번들러이다.

    패키지 번들러에는 Webpack, Rollup, Parcel이 존재한다.[비교1, 비교2, 비교3]

    TOAST UI에서 웹팩 가이드 문서를 쉽고 친절하게 써놓았다.

     

    1.3.2 CSS Sprites

    개요

    분류: Image

    CSS 스프라이트는 이미지 요청 수를 줄이기 위해 위해 나온 기법이다.

    배경 이미지들을 단일 이미지로 결합하고, CSS의 background-image와 background-positon속성을 사용해 원하는 이미지 세그먼트를 표시한다.

     

    스프라트를 만들때 몇가지 최적화 방법이 존재한다.

    • 스프라이트의 이미지를 세로가 아닌 가로로 정렬하면 파일 크기가 줄어 듭니다.
    • 스프라이트에 유사한 색상을 결합하면 PNG8에 맞도록 색상 수를 낮게 (256 색 미만) 유지할 수 있습니다.
    • "모바일 친화적"으로 이미지 사이에 큰 간격을 두지 마십시오. 
      이것은 파일 크기에는 영향을 미치지 않지만 사용자 에이전트가 이미지를 픽셀 맵으로 압축 해제하기 위해 더 적은 메모리를 필요로합니다. 100x100 이미지는 1 만 픽셀이며 1000x1000은 1 백만 픽셀입니다.

    NHN의 경우 CSM(CSS Sprites Matrix)라는 가이드라인을 통해 최적화를 한다.[Front-End 최적화의 끝판왕, CSM + Markup Complexity]

    • Matrix 배열: 10x10 형태의 이미지 배치, 공간 활용도 향상, 유지보수
    • 이미지 개수 제한: 페이지당 2개 이하
    • 해상도 제한: 3메가픽셀(1024x1024)
    • PNG 포맷

     

    그런데 HTTP/2 에서는 스프라이트가 안티패턴이 될 수 있다는 말이 있어 관련 자료를 찾아보게 되었다. [모바일 환경에서 이미지 스프라이트가 필요할까?, HTTP/2 arrives but sprite sets ain’t no dead]

     

    첫번째 링크에서 요약해놓은 논쟁점은 다음과 같다.

    1. 단일 이미지가 낫다!
      • 스프라이트 이미지는 이용하는 페이지 외의 불필요한 이미지 리소스를 모두 불러온다.
      • 스프라이트 이미지는 소수의 이미지 변경에도 모든 이미지의 브라우저 캐시가 무효화된다.
      • iOS 웹브라우저의 경우 25kb 이상의 요소는 캐싱하지 않는다.
    2. Image Sprite를 써야한다!
      • PNG 압축 알고리즘이 여러 개의 단일 이미지보다 한장의 스프라이트 이미지에서 더욱 효율적으로 동작한다.
      • HTTP/2는 로드 시간이 현저히 감소하지만 HTTP 프로토콜 향상만으로는 front-end 최적화의 유용성을 충분히 대체하기 어렵다.

    결론적으로 따지면 HTTP/2에서 로드 시간이 확연히 줄어들긴하나 효과는 있었다.

     

     

    해결방법

    위와 같이 스프라이트화된 이미지를 가지고 네개의 아이콘을 만들고 싶다면 다음과 같이 할 수 있다. [CSS 이미지 스프라이트]

    .up, .down, .right, .left { background: url("/examples/images/img_image_sprites.png") no-repeat; }
    .up { width: 21px; height: 20px; background-position: 0 0; }
    .down { width: 21px; height: 20px; background-position: -21px 0; }
    .right { width: 22px; height: 20px; background-position: -42px 0; }
    .left { width: 22px; height: 20px; background-position: -65px 0; }

     

    CSS Sprites: Image Slicing’s Kiss of DeathCSS로 이미지 스프라이트 구현하기, CSS Tricks

    도 잘 설명해주고 있다.

     

    사실 스프라이트의 가장 큰 단점은 스프라이트 이미지를 만들기 불편하고, 계산해서 쓰기 또한 힘들다는 것입니다.

    spritesmith가 스프라이트 이미지를 쉽게 만들고 관리할 수 있는 것으로 알려져 있는데 css-sprite-loader(Webpack Plugin)를 사용하면 background-size, background-position을 알아서 계산해서 대입해주며 이미지 크기(@2x, @4x...)별로 대응도 가능하다.

     

    예를들어

    .foo {
        background: url('../assets/gift.png?sprite');
    }

    이미지 경로 끝에 ?sprite를 넣으면

     

    .foo {
        background: url(/sprite.png?5d40e339682970eb14baf6110a83ddde) no-repeat;
        background-position: -100px -0px;
    }

    스프라이트화가 되며 위와 같이 알아서 할당이 되는 구조.

     

    SVG의 경우 svg-sprite와 JetBrains의 svg-sprite-loader를 쓸 수 있다.

    SVG usestacks를 알아두면 좋다.

     

    1.3.3 Image maps

    개요

    분류: Image

    이미지 맵(Image maps)[w3.org]은 클릭 가능한 영역이 여러가지 있는 하나의 이미지이다.

    좀 오래되어 보이지만.. 위와 같이 통으로 된 이미지에 각 페이지로 이동하는 링크를 달 때 사용할 수 있다.[Creating HTML Image Maps]

     

    스프라이트처럼 여러 이미지를 단일 이미지로 결합하여 HTTP 요청 수를 줄인다는 아이디어.

    그러나 이미지 맵은 페이지에서 메뉴바(Navigation Bar)처럼 이미지가 인접한 경우에만 작동하며 이미지 맵의 좌표를 정의하는 것은 지루하고 오류가 발생하기 쉽고 접근성이 떨어진다.

     

    인접한 이미지들에만 쓰는 것이 좋다지만 메뉴바에 이미지 맵을 사용하는 것도 액세스 할 수 없으므로 권장되지 않는다..

    그냥 이런 기법이 있었다는 것만 알고 넘어가는 것이 웬만하면 좋다.

     

    해결방법

    Image Map Creator나 다양한 생성기(Image Map Generator, Image map org, image maps com)이 있다.

     

    하지만 화면 크기가 상이한 기기들을 대응하려면 반응형 생성기가 필요하다.(Responsive Image Map Generator1, Responsive Image Map Generator2, Responsive Image Map Creator)

     

    기타 반응형 이미지 맵을 위해 나온 여러 도구들.

    Image-MapImage Map ResizejQuery RWD Image Maps

     

    1.3.4 인라인 인코딩

    개요

    분류: Content, Image

    인라인 인코딩(Inline Encoding)은 data: URL scheme바이너리 표현(Base64)으로 하드코딩하는 것이다.

     

    CSS파일의 크기가 커질 수 있지만 인라인 이미지를 (캐시된) 스타일 시트에 결합하면 HTTP 요청을 줄이고 페이지 크기를 늘리지 않을 수 있다.

     

    단, 스프라이트 하기 힘든 작은 아이콘들에 적용하는 것이 좋다.

    이미지는 랜더링을 막지 않지만 CSS의 해석시 렌더링을 막으며 캐시 가능성이 떨어진다. 파일의 크기가 증가하고 항상 이미지를 로드하며, 이미지 프리로드 등의 기술도 못쓴다는 점도 고려대상.[Base64 Encoding & Performance, Base64 인코딩과 성능]

     

    베이스64 인코딩 하기 귀찮은 것도 문제. [Data URIs]

     

    해결방법

    NodeJS에서는 Base64 Encoding in Node.js, node-base64-imagebase64-img등을 참고바란다.

    웹팩의 경우 url-loader를 이용하면 쉽게 만들 수 있다.

     

    다양한 이미지와 아이콘 사용 정리

    이쯤되면 도대체 어떠한 상황에 이미지를 어떻게 처리하는지 혼란스러울것 이다.

     

    - 단순한 이미지

    아이콘처럼 단순한 이미지는 벡터 그래픽으로 표현한다.

    @2x, @4x같이 해상도를 대응할 필요가 없고, 용량도 아낄 수 있다.

     

    벡터 그래픽이라면 SVG와 아이콘 폰트, CSS라는 세가지 선택지가 존재한다.

    우선, 가능하면 CSS로 작성하고, SVG와 아이콘 폰트에 대해 고민할 수 있는데..

    최근에는 SVG를 사용하는 것을 선호하는 편이다. [Inline SVG vs Icon Fonts, Icon Fonts vs SVGs - Which One Should You Use in 2018?, It’s 2019! Let’s End The Debate On Icon Fonts vs SVG Icons]

     

    React의 경우 SVG를 컴포넌트처럼 다룰 수 있기도 하고.

     

    - 작은 이미지

    10k 미만으로 작으면서 항상 불러와야 하는 이미지라면 Base64로 인코딩해서 사용하는 것도 괜찮다.

     

    - 기타 정적 이미지

    다양한 색상의 이미지들을 항상 불러와야 한다면 Image Sprite로 묶는다.

    (컨텐츠 이미지는 제외)

     

    - 압축과 포맷

    가능하면 이미지를 압축하여 사용하도록 한다.

    움직이는 GIF는 ffmpeg를 이용해 영상으로 변환하고 나머지 일반 이미지는 WebP를 사용하면 좋다.

     

    1.3.5 다중문서에 구성요소들을 묶기

    개요

    분류: Mobile
    이메일의 첨부파일과 같이 다중문서(Multipart Document)에 구성요소들을 묶는 것은 한 개의 HTTP 요청에 여러 개의 구성요소들을 불러오는데 도움이 된다.

    HTTP 요청들이 비용(시간&자원)이 발생하는 것을 막기 위해서다.

     

    사용자가 이 기술을 사용할 때, 사용자 에이젼트(아이폰은 지원하지 않음)가 이러한 기능을 지원하는지 먼저 확인하는 것이 필요.

     

    YDN에서 다중문서라 하길래 도대체 무엇인가 하니..

    MHTML을 뜻한다구.

     

    일반 웹문서가 HTML코드와 참조하는 별도 파일들(이미지, 음성, 영상)으로 구성된다면 MHTML은 MIME 포맷으로 인코딩해 합쳐진 것이 특징.

     

    현재 의미있는 기술은 아니다.

     

    1.4 나누기

    크기가 크거나 모듈화나 캐싱이 가능한 것은 나누는 것이 좋다.

     

    1.4.1 구성요소들을 도메인별로 분리

    개요

    분류: Content

    DNS 요청을 줄이라는 원칙에서 언급했듯 요소들을 분할하는 것은 병렬 다운로드를 극대화할 수 있다.(HTTP 1.1 기준 한 도메인에서 6개가 최대)

    DNS 조회의 불이익 때문에 2-4개 이상의 도메인을 사용하고 있지는 않은지 확인하는 것이 핵심.

     

    예를 들어, www.example.org에서 HTML과 동적 콘텐츠를 호스트할 수 있고, static1.example.org와 static2.example.org 사이의 정적 구성 요소를 분할할 수 있다.

    자세한 내용은 Tenni Theurer와 Patty Chi의 "Maximizing Parallel Downloads in the Carpool Lane"를 참조해라.

     

    해결방법

    만약 1개의 도메인에서 모두 다 서비스하고 있다면 라이브러리는 CDN을 사용해보는 것이 어떨까?

    물론 HTTP 1.1까지의 이야기.

     

    1.4.2 JavaScript와 CSS를 외부로 두기

    개요

    분류: Javascript, CSS

    이러한 성능 규칙 중 많은 부분이 외부 컴포넌트 관리 방법을 다룬다.

    그러나 이러한 고려 사항이 발생하기 전에 떠오르는 기본적인 질문이 있다.

    JavaScript 및 CSS가 외부 파일에 포함되어야 할까? 아니면 페이지 자체에 인라인되는 것이 좋을까?

     

    실제 환경에서 외부 파일을 사용하면 JavaScript 및 CSS 파일이 브라우저에 의해 캐시되므로 일반적으로 더 빠른 페이지를 생성한다.

    HTML 문서에 인라인 된 JavaScript 및 CSS는 HTML 문서가 요청 될 때마다 다운로드된다.

    이렇게하면 필요한 HTTP 요청 수가 줄어들지만 HTML 문서의 크기는 커진다.

    반면, JavaScript 및 CSS가 브라우저에 의해 캐시 된 외부 파일에 있으면 HTTP 요청 수를 늘리지 않고 HTML 문서의 크기가 줄어든다.

     

    인라인 인코딩등에서도 계속 고민해왔던 내용이라는 것을 알 수 있으며 접근방법도 동일하다.

    따라서 핵심 요소는 요청 된 HTML 문서 수와 관련하여 외부 JavaScript 및 CSS 구성 요소가 캐시되는 빈도이다.

    이 요소는 정량화하기는 어렵지만 다양한 메트릭을 사용하여 측정 할 수 있다.

    사이트의 사용자가 세션 당 여러 페이지보기를 가지고 있고 많은 페이지가 동일한 스크립트 및 스타일 시트를 재사용하는 경우 캐시 된 외부 파일의 잠재적 이점이 더 크다.

     

    많은 웹 사이트가 이러한 메트릭의 중간에 있다.

    이러한 사이트의 경우 가장 좋은 해결책은 일반적으로 JavaScript 및 CSS를 외부 파일로 배포하는 것이다.

    인라인을 선호하는 유일한 예외는 Yahoo! 의 첫 페이지 및 My Yahoo! 처럼 세션 당 페이지 뷰가 거의없는 (아마도 하나만) 홈 페이지는 JavaScript 및 CSS를 인라인하면 최종 사용자 응답 시간이 더 빨라질 수 있다.

     

    일반적으로 많은 페이지뷰 중 첫 번째인 프론트 페이지에는 인라인이 제공하는 HTTP 요청 감소와 외부 파일을 사용하여 얻을 수있는 캐싱 이점을 활용하는 기술이 있다.

    이러한 기술 중 하나는 프론트 페이지에서 JavaScript 및 CSS를 인라인하는 것이지만, 페이지 로드가 완료된 후 동적으로 외부 파일을 다운로드하는 것이다.

    후속 페이지는 이미 브라우저 캐시에 있는 외부 파일을 참조한다.

     

    해결방법

    특수한 경우가 아니라면 외부에서 불러오도록 분리하자.

     

    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 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: 리엑트 개발자에게 익숙한 방식을 제공

     

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

    개요

    분류: Server

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

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

     

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

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

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

     

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

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

     

    해결방안

    PHP, ASP, JSP와 같은 템플릿 엔진들은 다음처럼 플러싱을 할 수 있다. [HTTP Archive: adding flush]

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

     

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

     

    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에서 스타일 무효화]

     

    선택자 복잡성

    예를 들어 다음은 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]

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

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

    내가 사용한다면 Linaria를 쓸 듯.

     

    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를 적용하도록 만들 수도 있긴한데,  전체를 로드하는 시간이 더 빨라 좋은 방법이 아니다.

     

    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는 제외되는 것 같다.

     

     

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

    • Custom elements + HTML Templete: 말 그대로, 재사용에 특화가 되었기 때문에 최적화 가능성이 있을 듯
    • Shadow DOM: 스코프를 분리하기 때문에 CSS 규칙이 간단해져 최적화 가능성이 있음
    • HTML Import: async 속성을 통해 비동기적으로 가져올 수 있으며, 파싱을 차단하지 않음 [HTML Imports, Stackoverflow]

    안타깝게도 현재는 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]

     

    3. Layout 및 렌더링

    Layout 발생 빈도 최소화 및 비용 최소화와 CPU 처리 효율화, UX 트릭으로 나뉜다.

    레이아웃에 영향을 미치는 것들에 대한 정리는 What forces layout/reflow란 글이 잘 설명해준다.

     

    이 중 리플로우에 영향을 미치는 유명한 CSS 요소들을 뽑아보자면 [CSS Triggers]

    • 창 크기 조정
    • 폰트 변경
    • CSS 추가와 삭제
    • input 에 입력시 내용 변경
    • CSS 가상 클래스 활성화
    • 클래스 속성, 스타일 속성과 DOM 조작
    • offsetWidth, offsetHeight 계산

    보통 JS-리플로우(Layout)-페인트-컴포지터의 과정으로 렌더링이 되기 때문에 동일한 동작을 수행시

    컴포지터를 업데이트하는 것이 페인트가 필요한 것보다 낫다.

     

    3.1 발생 빈도 최소화

    3.1.1 DOM 액세스 최소화

    개요

    분류: Javascript 
    JavaScript로 DOM 요소에 액세스하는 속도가 느리다.

     

    DOM과 관련된 영향을 고려하며 프로그래밍 해야한다.

    • 부모-자식 관계: 부모 엘리먼트가 가변적인 크기를 가질 때 자식 엘리먼트의 크기를 수정하면 부모에게 영향
    • 같은 위치: 여러 엘리먼트가 인라인일때 첫 번째 엘리먼트의 크기 수정으로 나머지 엘리먼트에 영향
    • 숨겨진 엘리먼트: 숨겨진(display: none) 스타일이면 돔 조작이나 스타일 변경시에도 레이아웃, 리페인트가 생기지 않음
      visibility: hidden일 경우 리페인트는 발생하지 않아도 레이아웃은 계산

     

    그리고 다음 같은 원칙을 생각해볼 수 있다. [DOM access optimization, 무한 DOM 렌더링 최적화 경험기, html5 성능최적화 방법들]

    • DOM 연산 결과 캐싱
      접근했던 요소(element)에 대한 참조를 캐시
    • DOM 변경 시 개별 노드단위 반복변경보다는 배칭 작업
    • DOM 노드 변환은 오프라인에서 수행
      노드(node)는 오프라인으로 업데이트 한 다음 트리에 추가(변경노드 떼기 -> 연산 -> 붙이기)
    • JavaScript로 레이아웃을 고정하지 않기

     

    - 강제 동기 레이아웃 및 스래싱 피하기

    레이아웃은 원래 비동기적이지만, 특정 속성을 읽을 때 최신 값을 계산하는 등 레이아웃이 동기적으로 발생할 때가 있다. [크고 복잡한 레이아웃 및 레이아웃 스래싱 피하기, Preventing 'layout thrashing]

    스타일 변경 후 offsetHeight나 offsetTop처럼 계산된 값을 속성으로 읽을 때가 대표적인 예.

     

    연속적인 강제 동기 레이아웃이 발생해 레이아웃 스래싱[한글]까지 발생하면 성능은 더욱 저하된다.

     

    • DOM 트리 상위노드를 변경시 하위 노드에 영향

    이므로 변경범위를 최소화 한다는 것을 고려하도록 하자.

     

     

    해결방법

    - DOM 연산 결과 캐싱

    DOM에 접근하는 것을 매번 이용하는 것은 좋은 선택이 아니다.

    document.getElementById('box').style.top = "10px";
    document.getElementById('box').style.left = "10px";
    document.getElementById('box').style.color = "lightgrey";

     

    어찌보면 당연하겠지만, 변수로 만들어놓으면 된다.

    const boxSty = document.getElementById('box').style;
    boxSty.top = "10px";
    boxSty.left = "20px";
    boxSty.color = "lightgrey";

     

    HTMLCollection처럼 실시간 업데이트되는 것도 마찬가지.

    HTMLCollection의 여러값들에 지속적으로 접근해야 하는 경우 Array로 바꾸는 것도 고려할만 하다. [Most efficient way to convert an HTMLCollection to an Array]

     

    - 개별노드 변경보다 배칭 작업

    개별 노드를 반복변경하기보다 배칭(Batching, 일괄처리)을 하는 것이 좋다.

    유니티 같은 엔진에서 잘 사용하고 있는 기법. [드로우 콜 배칭, Dynamic Batching이 효율적인가]

     

    CSS 활용

    앞선 예제는 CSS를 사용해 한번에 업데이트가 가능하다.

    const boxSty = document.getElementById('box').style;
    boxSty.classList.add('boxstyles')
    .boxstyles {
      top: 10px;
      left: 20px;
      color: lightgrey;
    }

     

    DocumentFragment 활용

    다음코드에서 appendChild(쓰기)와 getBoundingClientRect(읽기)의 최적화가 목표.

    function noBatch(elements) {
      elements.forEach(el => {
        // This is alternating write and read
        this.parentElement.appendChild(el);
        el.getBoundingClientRect();
      });
    }

     

    DocumentFragment를 사용하면 리플로우를 일으키지 않으며 처리가 가능하다. [JavaScript DocumentFragment]

    function batch(elements) {
      // First write
      const fragment = document.createFragment();
      elements.forEach(el => {
        fragment.appendChild(el);
      });
      this.parentElement.appendChild(fragment);
      
      // Then read
      elements.forEach(el => {
        el.getBoundingClientRect();
      });
    }

     

    DOM 노드 변경은 오프라인으로

    많은 수의 DOM을 조작해야 한다면 DocumentFragment와 마찬가지로 직접적인 돔 조작을 줄여야 한다.

    이때 생각해볼 만한 것은

    1. DOM에서 때어냄
    2. 연산
    3. 다시 붙임

    의 과정을 수행하는 것이다.

     

    jQuery의 detach()가 대표적인 예.[제이쿼리 성능 향상하기(performance), 다른 삭제와 비교]

    바닐라의 경우 ba-detach.js를 참고하면 될 듯.

     

    뭐 사실, GTK의 Pixmap(페인트가 필요 없음)과 Pixbuf(X11 서버에 접근 불필요) 같은 것을 보면 새로운 개념은 아닌듯 하다.

     

    - 강제 동기 레이아웃 및 스래싱 피하기

    offsetWidth처럼 강제 동기 레이아웃 속성들은 되도록 사용하지 않는 것이 좋다.

    하지만 써먹어야 하는 경우가 있기에.. 몇가지 예를 들어본다.

     

    순서변경

    다음 코드는 box의 너비(Width)를 기록하기 위한 코드이다.

    // Schedule our function to run at the start of the frame.
    requestAnimationFrame(logBoxWidth);
    
    function logBoxWidth() {
      box.classList.add('super-big');
    
      // Gets the width of the box in pixels
      // and logs it out.
      console.log(box.offsetWidth);
    }

    코드를 보면 높이를 요청하기 전에 box의 클래스를 바꾸었기 때문에 스타일 변경이 적용되어 있어야 하는 상황.

    스타일 변경후의 결과를 위해 강제적으로 레이아웃이 동기화가 되어야 한다는 것을 알 수 있다.

     

    만약 다음처럼 순서를 바꾼다면, 전 프레임의 레이아웃 값을 사용할 수 있어 비용을 아낄 수 있다.

    function logBoxWidth() {
      // Gets the width of the box in pixels
      // and logs it out.
      console.log(box.offsetWidth);
    
      box.classList.add('super-big');
    }

     

    변수로 선언

    매번 offsetWidth를 호출하면 스래싱이 생겨서 성능이 확 줄어든다.

    function resizeAllParagraphs() {
      for (let i = 0; i < paragraphs.length; i += 1) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
      }
    }

     

    역시, 변수처리를 하면 해결되는 부분.

    function resizeAllParagraphs() {
      const width = box.offsetWidth;
    
      for (let i = 0; i < paragraphs.length; i += 1) {
        paragraphs[i].style.width = width + 'px';
      }
    }

     

     

    - 라이브러리

    레이아웃 스레싱

    레이아웃 스레싱 문제를 해결하기 위해 DOM을 자동으로 읽고 써주는 [Preventing 'layout thrashing']

    을 사용할 수 있다.

     

    AMP의 Vsync 클래스도 비슷한 원리인듯. [AMP는 어떻게 웹 페이지의 성능을 높일 수 있나]

     

    DOM 조작

    jQuery 독점기를 깨고 나오는 각종 라이브러리들은 돔조작을 간편하고 효율적으로 만드려고 눈물나는 노력들의 산물이다. [Change And Its Detection In JavaScript Frameworks, React Virtual DOM vs Incremental DOM vs Ember’s Glimmer: Fight]

     

    Backbone.jsExt JS, Dojo는 데이터 모델의 상태를 변경하여 반영하려했는데, 변경의 이벤트의 대해 다시 렌더링을 해야할지 결정하고 해결해야하는 것은 프로그래머가 해야 한다.

    Ember.js는 데이터 바인딩을 통해 이벤트가 수행할 리스너를 등록하여 동기화를 편하게 만들었다.

    AngularJS는 더티체크를 수행하여 마지막 순간에서 변경되었는지를 체크한다.

    그러나 매번 참조한 값이 변경되었는지 확인하는 것은 빈번히 재렌더링이 발생할 시 비용이 크다.

     

    React는 매우 가벼운 Virtual DOM을 매번 생성 후, 비교후 실제 돔조작을 하는 방식이다.

    장점은 변경사항을 추적할 필요가 없다는 것.

    Ember의 Glimmer 엔진에는 Virtual DOM에서 영감을 받은 Stream Tree를 이용해 변경점을 업데이트하고, Incremental DOM는 실제 DOM과 메모리상 DOM을 비교하여 업데이트 한다.[Glimmer: Blazing Fast Rendering for Ember.js(part1, part2), Incremental DOM 101: What is it and why I should care?]

     

    Om이나 Reagent는 변경이 안된 것들을  Persistent 데이터구조를 이용해 파악하여 재사용하여 성능에 이점이 있다.[Understanding Clojure's Persistent Vectors, pt. 1, Persistent Data Structures and Managed References]

    단점은 사람들이 익숙하지 않은 LISP 계열의 Closurescript를 이용한다는 것.

     

    이 아이디어는 불변성을 관리하는 라이브러리인 Immutable JS, Immer를 탄생시켰고, Angular에도 적용되고 있다. [Change Detection in Angular 2]

     

    자료를 찾다가 벤치마크 자룔르 보게 되었는데 몇가지 흥미로운 프레임워크가 존재한다.[A RealWorld Comparison of Front-End Frameworks 2020]

     

    최근에 뜨고 있는 SvelteElm은 컴포넌트들을 컴파일하여 매우 효율적인 코드로 변환해 Virtual DOM의 오버헤드를 제거하자는 것이다.[Frameworks without the framework: why didn't we think of this sooner?, Svelte 3: Rethinking reactivity, svelte가 빠른 이유, Blazing Fast HTML, Elm — 웹 앱 전문 함수형 프로그래밍 언어]

     

    이중 Elm의 아키텍처는 언어적으로 상태관리를 하도록 만들어 Hyperapp등에도 영향을 주었다.  [Subscriptions, The Elm Architecture (TEA) animation, Everything You Need To Know About Hyperapp JS]

     

    개인적으로 Elm의 아키텍처를 가장 쉽게 풀어낸 것이 Apprun이나 re-frame인 듯하다. [AppRun - Architecture, Why React & re-frame are fast in 8 Performance Hacks(영상)]

     

     

    3.1.2 이벤트 핸들러를 잘 개발하기 with API

    개요

    분류: Javascript
    DOM 트리의 다른 요소에 너무 많은 이벤트 핸들러가 첨부되어 너무 자주 실행되거나 실행 간격이 크기 때문에 페이지의 응답성이 떨어지는 경우가 있다.

     

    해결방법

    - 이벤트 위임

    이벤트 위임(event delegation)을 사용 하는 것이 좋은 방법. [왜 이벤트 위임(delegation)을 해야 하는가?, 이벤트 버블링, 이벤트 캡처 그리고 이벤트 위임까지, Event Delegation]

    안에 10 개의 버튼이있는 div경우 각 버튼마다 하나의 핸들러 대신 하나의 이벤트 핸들러만 div 래퍼에 연결하는 방식을 쓰자.

    이벤트 핸들러가 너무 많으면 반응속도가 느려진다.

    이벤트가 버블업되어 이벤트를 포착하고 이벤트가 시작된 버튼을 파악할 수 있다.

     

    - onload vs DOMContentLoad
    DOM 트리로 무언가를 시작하기 위해 onload 이벤트를 기다릴 필요가 없다.

    트리에서 사용할 수 있도록 액세스하려는 요소만 있으면 된다.

    모든 이미지가 다운로드 될 때까지 기다릴 필요도 없다.

    Web Page Load Time

     

    DOMContentLoaded는 onload 대신에 사용할 수 있다. [문서의 로드시점 - onload, DOMContentLoaded, DOMContentLoaded vs load]

     

    자세한 정보는 Julien Lecomte이 작성한 "High Performance Ajax Applications"를 참고.

     

    - requestAnimationFrame(), requestIdleCallback() 사용

    requestAnimationFramerequestIdleCallback을 사용하면 작업을 작은 조각으로 나누어 처리할 수 있다. [시각적 변화에 requestAnimationFrame 사용, Using requestIdleCallback, 브라우저는 vsync를 어떻게 활용하고 있을까]

    따라서 장기 실행중인 자바스크립트가 메인 스레드를 차단하여 반응성을 낮추는 일을 막을 수 있다.

     

    이 둘은 setTimeout, setInterval을 대체해 사용하는데 성능 향상폭이 상당하다.[남다른 개선방법을 다시 보여준 페이스북의 React Fiber, 100,000개의 아이템도 거뜬한 셀렉트박스 만들기]

     

    jQuery requestAnimationFrame은 requestAnimationFrame을 사용해 jQuery의 animate 반응성을 높이는 대표적인 라이브러리.

    리엑트에서 requestAnimationFrame을 사용하려면 다음글이 유용할 것이다. [Using requestAnimationFrame with React Hooks]

     

    - IntersectionObserver 사용

    Lazy 로딩시 scroll 이벤트나 resize 이벤트를 받아서 getBoundingClientRect()같은 DOM API를 사용하여 뷰포트 위치를 계산하는 것이 일반적이지만 IntersectionObserver를 사용하는 것이 효율적.[IntersectionObserver's Coming into View, Intersection Observer 간단 정리하기]

     

    구형 브라우저를 위한 폴리필이 존재한다. [IntersectionObserver polyfill]

     

    - pointer 이벤트나 touch 이벤트 사용

    모바일에서는 더블탭이 될수도 있고, 제스쳐와 같은 동작을 위해 클릭 이벤트는 300ms를 대기 시간으로 둔다.

     

    touchstart - touchmove - touchend - mouseover - mousemove - mousedown - mouseup - click

    의 순으로 이벤트가 일어난다는 점을 고려하면 [Supporting both TouchEvent and MouseEvent, Handling Gesture Events, 사이트에 터치추가]

    보다시피 훨씬 빠르다.

     

     

    click보다는 touchstart, touchend를 이용하는 것이 좋다.

     

    다음처럼 터치이벤트를 지원하는지 확인하거나

    const clickEvent = (() => {
      if ('ontouchstart' in document.documentElement === true) return 'touchstart';
      else return 'click';
    })

     

    구형 브라우저는 fastclick과 같은 라이브러리를 사용해 응답성을 개선할 수 있다. [300ms tap delay, gone away]

     

    그리고 Pointer Event란 터치이벤트보다 좋은 것이 나왔다. [포인터 이벤트, getting touchyPointer Events, iOS 13의 Pointer 이벤트, Pointer Events in React — The Why, How, and What, Enabling Cross-Platform Touch Interactions: Pointer vs. Touch Events]

    마우스, 터치 뿐만아니라 팬까지 모두 사용할 수 있도록 만든 이벤트라는데 Touch/pointer tests and demos(이벤트 리스너)로 테스트 해보자.

    비교표와 순서

    둘을 대체 가능할 뿐만아니라 클릭 이벤트의 300ms 지연도 막을 수 있다.(우선순위가 가장 높음)

     

    역시 PEP(Pointer Events Polyfill)처럼 폴리필이 존재.

     

    3.1.3 로드 후(Post-load) 구성 요소

    개요

    분류: content
    페이지를 곰곰히 살피다보면 "처음에 페이지를 렌더링하기 위해 반드시 필요한 것은 무엇인가?"에 대한  고민을 할 수 있다.

    나머지 내용과 구성 요소는 기다리게 만들어도 된다.

     

    해결방법

    JavaScript는 onload 이벤트 전후에 분할하기에 이상적인 후보이다.

    function afterPageLoad(){
       console.log('after page load');
    }
     
    window.onload = function afterPageLoad();

    예를 들어, 드래그 앤 드롭 및 애니메이션을 수행하는 JavaScript  코드 및 라이브러리가 있는 경우 초기 렌더링 후 페이지 드래깅 요소가 오기 때문에 대기 할 수 있다.

     

    Post load의 후보를 찾을만한 다른 장소로는 fold 아래에 숨겨진 콘텐츠 (사용자 작업 후에 표시되는 콘텐츠)와 이미지가 있다.

    (이미 언급했죠?)

     

    그런데 성능 목표는 다른 웹 개발 모범 사례와 일치 할 때 이상적이란 사실은 염두해야 한다.

    이 경우 점진적 향상이라는 아이디어는 JavaScript를 사용하여 UX을 향상시킬 수 있지만, JavaScript 없이도 페이지가 작동하는지 확인해야한다는 것을 뜻한다.

    따라서 페이지가 제대로 작동하는지 확인한 후 드래그 앤 드롭 및 애니메이션과 같은 부가기능을 제공하는 Post Load 스크립트로 페이지를 향상시킬 수 있다.

     

    3.1.4 Flexbox, Grid 활용

    개요

    분류: CSS

    CSS로 레이아웃을 구성하는 방법은 [CSS 레이아웃 입문서]

     

    를 이용하였다.

     

    그러나 최신 CSS의 방식을 사용하면 훨씬 빠르다. [이전 레이아웃 모델 대신 Flexbox 사용]

     

    다음은 1300개의 상자들의 레이아웃을 짠 것.

    3.544ms로 14.289 ms보다 훨씬 빠름

     

     

     

    3.1.5 Contain 속성 활용하기

    개요

    분류: CSS

    contain은 웹페이지에서 선택된 하위 트리를 문서의 나머지 영역과 분리하는 기능을 가지고 있다. [CSS Containment Module, CSS Containment in Chrome 52, Helping Browsers Optimize With The CSS Contain Property]

    분리되면 너무나 당연하게도 성능 향상이 된다!!

     

    첫번째 참조 링크에 따르면 3~6ms에서 0.3~1.9ms로 약 2~10배까지 차이가 난다.

    텍스트 요소를 바꿔 리플로우 발생[코드펜]

     

    두번째 링크의 결과는 차이가 더 많이 난다. ㅎㅎ

     

    대략적으로만 속성을 알아보자면

    • strict: size + layout + paint
    • content: size + paint
    • size: 상위요소는 하위요소의 크기와 독립적임
    • layout: 하위요소는 상위요소 안에서만 움직임
    • style: 요소와 해당 요소에 영향을 끼치지만, 그 이상 벗어나진 않음(서로 영향을 끼치지 않음, 독립적)
    • paint: 하위요소는 상위요소 안에서만 보임

    처럼 이루어져 있다.

     

    쉐도우 돔을 사용해도 효과가 이리 나타나야 할텐데..

     

    3.1.6 대형/연속적 리스트 최적화

    개요

    분류: Content

    구글 검색시 인피니티 스크롤, 페이스북이나 인스타그램의 타임라인, 핀터레스트의 Masonry 레이아웃은 끝없는 데이터를 연속적으로 보여주는 예다. [Virtualize large lists with react-window]

     

    이를 어떻게하면 효율적으로 구현할 수 있을까?

     

    해결방법

    대형인 데이터를 최적화하여 보여주는 방법은 고전적인 텍스트 에디터에서도 많이 고민해왔던 문제이다.

    프로그래밍 경력이 짧은 본인의 경험만 생각해도 머신러닝용 데이터나 로그파일을 보아야 할 때 성능 때문에 문제가 생긴적이 있었으니까.

     

    이맥스의 VLF(VLFI)가 대표적인 예인데 OS에서 가상 메모리의 페이징 기법처럼 사용하여 커다란 데이터를 필요한 만큼만(Lazy하게) 다룰 수 있다.

    Virtual Memory(한글 설명)

     

    약간 더 직관적으로 바라보고, UX를 생각해보자.

    위의 작업을 페이지와 프레임단위로 사용자가 사용할 페이지들만 메모리에 올려놓고, 보여주는 것은 뷰에서 현재 사용할(Visible Window, Viewport) 또는 곧 사용할 리스트(Realization Window)만 보여주자는 이야기다.

    Realization Window처럼 리스트 여백을 남겨두는 이유는 사용자가 스크롤할 때 곧장 보여줄 수 있기 때문.

    ListView basics and virtualization concepts

     

    어차피 Realized View까지 한정된 갯수의 아이템만 사용할 것이라면, 각 Item을 매번 재생성하는 것은 비효율적이지 않을까?

    안드로이드의 ReycylerView나 IOS의 UICollectionView는 Item을 재사용(recyler, reuse)한다. [RecyclerView(리사이클러뷰)의 원리와 사용법(feat. Kotlin), 안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView), Kotlin & RecyclerView for High Performance Lists in Android, RecyclerView로 목록 만들기]

    ListView vs RecyclerView

     

    보여지는 각 Item을 View Holder 객체에 저장해두고, View Holder 내부에서 바꿔야 할 데이터들만 바인딩하여 바꿔치기만하면, 매번 Item을 제거하고 다시 생성할 필요가 없다.

    Using the ViewHolder Pattern

     

    관련 코드설명을 함께 사용하고 싶다면 다음 설명이 깔끔하다. [Tips & Tricks for Highly Performing Android ListViews]

     

    여기에 스크롤 성능을 더 향상시키위해 재사용하기전에 약간의 텀을 두고, 재사용 했을시 캐싱, 새로운 아이템에 대한 프리페칭등을 적용할 수도 있다. [iOS10의 프리-패칭 API로 부드러운 스크롤 증진하기, UICollectionView Tutorial: Prefetching APIs, UICollectionViewCell의 LifeCycle과 PreFetching]

     

    자 다음 사진으로 정리해보자.

    • Data List: 물리적 메모리
    • Adapter: 메모리 맵
    • ViewHolder: 페이지

    가 보이는가??

     

    React의 경우 다음을 사용가능하다. [Windowing wars: React-virtualized vs. react-window]

     

    3.1.7 React 최적화

    개요

    분류: Javascript

    여기에서는 리엑트의 렌더링성능을 최적화시키는 방법들에 대해 다루려한다. [How to greatly improve your React app performance, React 렌더링 이해 및 최적화 (With Hook)]

     

    리엑트는 클래스 컴포넌트나 함수형 컴포넌트를 사용하며 둘의 사용방법이 다르기 때문에 비교가 필요하다.

    보통 클래스 컴포넌트에 존재하는 라이프 사이클이 함수형 컴포넌트에는 없기 때문에 Hooks를 사용한다.

    라이프사이클과 Hooks의 관계는 대략 다음과 같다.

    https://black7375.tumblr.com/post/616396328590786560/%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EA%B8%80%EC%9D%84-%EC%A0%81%EC%9D%84-%EB%95%8C-%EC%95%84%EB%AC%B4%EB%9E%98%EB%8F%84-%EB%94%B0%EB%A1%9C-%EB%B6%84%EB%A6%AC%ED%95%B4-%EC%A0%81%EB%8A%94%EA%B2%8C-%EB%82%98%EC%9D%84-%EB%93%AF%ED%95%98%EC%97%AC-%EC%9E%91%EC%84%B1%ED%95%98%EA%B2%8C-%EB%90%98%EC%97%88%EB%8B%A4

     

    해결방법

    - 데이터 불변성 유지

    ES6의 Spread Syntax나 바벨의 Object Spread Syntax를 이용하면 쉽게 데이터 불변성을 유지하면서 값을 업데이트할 수 있다.

    // spread syntax
    handleClick() {
      this.setState(state => ({
        words: [...state.words, 'marklar'],
      }));
    };
    
    // object spread syntax
    function updateColorMap(colormap) {
      return {...colormap, right: 'blue'};
    }

     

    - 컴포넌트 메모이제이션

    shouldComponentUpdate(): 클래스

    shouldCompontnentUpdate는 현재 props와 state와 다음 props, state를 비교후, 렌더링할 것인지 말것이지 결정한다. [재조정을 피하세요]

    true는 렌더링. false면 렌더링을 하지 않는다.

    C2에서 false를 반환하여 렌더링을 하지 않음

     

    React.PureComponent: 클래스

    PurecomponentShallow Compare(얕은 비교)를 이용하는 것과 같다.

    export class SampleComponent extends React.Component {
      shouldComponentUpdate(nextProps, nextState) {
        return shallowCompare(this, nextProps, nextState);
      }
    
      render() {
        return <div className={this.props.className}>foo</div>;
      }
    }

     

    그렇다면 얕은 비교는 어떻게 이루어질까?

    자바스크립트를 처음 배울 때 객체와 레퍼런스들을 다루는 것과 같다.

    Are javascript object variables just reference type?

     

    s2는 s1의 포인터를 사용하는 것과 같아 같은 레퍼런스를 가르치므로 같으며,

    s3는 값은 같으나 전혀 다른 객체이므로 달랐던 것처럼.

    const s1 = {text: "first"};
    const s2 = s1;
    const s3 = {text: "first"};
    
    s1 === s2; // true
    s1 === s3; // false

     

    PureComponent는 props와 state에 대해 얕은 비교를 수행한다.

    만약 깊은 비교가 필요하면 react-fast-compare나 fast-deep-equal를 이용해보자.

     

    React.memo: 함수형

    React.memo는 PureComponent와 shouldComponentUpdate의 일부를 사용하도록 만들어졌다. [React.memo() 현명하게 사용하기]

    첫번째 인수로 PureComponent로 사용할 컴포넌트, 두번째 인수로 같은 컴포넌트인지 확인하기 위한 비교할 콜백 함수를 넣을 수 있다.

    차이라면, props에 대해서만 얕은 비교를 수행하며, shouldComponentUpdate와 정반대로

    • true: 같으므로 렌더링 하지 않음
    • false: 다르므로 렌더링

    이다.

     

    다음은 React.PureComponent나 React.memo를 사용하면 좋은 상황들.

    • 순수 컴포넌트(Pure Component): props의 값이 같으면 렌더링이 같을 때.
    • 렌더링이 빈번히 일어날 때.
    • 얕은 비교를 할 필요가 있을 경우.

     

    반대로 매 렌더링마다 달라지는 경우는 불필요한 비교과정만 늘어난다.

     

    - 이벤트 리스너와 값 메모이제이션

    컴포넌트 밖에서 정의: 공통

    React.PureComponent나 React.memo를 사용시 해당 컴포넌트에 의존하지 않는다면 컴포넌트 밖에 정의하면 참조 값이 같으므로 새로운 함수나 값이 생성되지 않는다. [이벤트 리스너 캐시를 이용한 React 성능 향상]

     

    this로 바인딩: 클래스

    React.PureComponent를 사용시 컴포넌트 내부에 함수를 정의하고, this로 바인딩하면 같은 객체이므로 새로운 함수나 값이 생성되지 않는다.

     

    useCallback/useMemo: 함수형

    useCallback은 이벤트 리스너를 useMemo는 값을 메모이제이션한다. [Optimize Your React Functional Components with useCallback and useMemo]

     

    useState 대신 useReducer 사용: 함수형

    useReducer는 콜백 대신 dispatch를 전달한다. [How to avoid passing callbacks down?, How to use useReducer in React Hooks for performance optimization]

    다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우나 다음 state가 이전 state에 의존적인 경우에 보통 useState보다 useReducer를 선호합니다. 또한 useReducer는 자세한 업데이트를 트리거 하는 컴포넌트의 성능을 최적화할 수 있게 하는데, 이것은 콜백 대신 dispatch를 전달 할 수 있기 때문입니다.

    이때 dispatch는 렌더시 변화하지 않으므로 일종의 메모이제이션으로 볼 수 있다.

     

    Flux의 Dispatcher 문서에서는 다음과 같이 차이를 정의한다.

    • 콜백은 이벤트를 개별적으로 구독하지 않는다. 모든 데이터 변동은 등록된 모든 콜백에 전달된다.
    • 콜백이 실행될 때 콜백의 전체나 일부를 중단할 수 있다.

     

    - React.lazy/React.Suspense 사용

    React.lazy를 사용하면 컴포넌트를 동적으로 불러올 함수를 등록할 수 있다.

    React.Suspense로 렌더링이 준비되지 않은 컴포넌트를 불러오고, fallback으로 스피너를 사용할 수 있다.

     

    Racket과 같은 언어에서 delay/lazyforce의 관계와 같다.

     

    - Context 사용

    Context를 사용하면 props를 바로 전달할 수 있어 쓸데없이 렌더링이 이루어지는 것을 막을 수 있다. [Avoiding unnecessary renders with React context, React Context 알아보기]

     

    3.2 처리 효율화

    3.2.1 워커나 워클릿 활용하기

    개요

    분류: Javascript

    웹워커는 무거운 연산들을 메인 쓰레드와 분리시켜 백그라운드에서 처리할 수 있도록 도와준다. [멀티스레드를 위한 자바스크립트 프로그래밍 웹 워커(요약), Web Worker 간단 정리하기, Web workers vs Service workers vs Worklets, The main thread is a bad place to run your code, Html5 web workers]

    • 처리 집약적 수학 계산
    • 대용량 데이터 세트 정렬
    • 데이터 작업(압축, 인코딩/디코딩, 오디오 분석, 이미지 픽셀 변환)
    • 트래픽 높은 네트워크 통신(네트워크 데이터 처리, 캐싱과 프리페칭)
    • 백그라운드 I/O, 웹사이트 폴링(Polling)

    등이 대표적인 예. [JavaScript 프로그램 성능 향상 ] 

     

    워커의 종류를 나누어 보자면 다음과 같다.

    웹워커나 서비스 워커를 사용하면 계산이 많이 필요한 것이나 캐싱이 필요한 것들에서 큰 폭으로 성능을 높힐 수 있다.

    웹 워커 성능 향상 예[Web Workers with the Angular CLI, Optimizing JavaScript Application Performance with Web Workers]

    서비스 워커 성능 향상 예[Progressive Web App Libaries in Production, 앱 셸 모델, Service workers in Depth]

     

     

    하지만 메인(UI) 쓰레드가 아니기 때문에

    • Window Object
    • Document Object
    • Parent Object

    를 사용하지 못한다는 점은 알고 있어야 한다.

     

    워커랑 비슷하지만, 가볍고 렌더링시에도 사용할 수 있는 Worklet도 있다.

    가장 큰 단점은 아직 지원하는 브라우저가 많지 않다.

    한가지 주의할 점은 3D 렌더링을 WebGL과 같이 사용할 수 없으며 WebGL의 GLSL 코드를 사용해야 한다.

     

    정리하자면

    • 전용 워커: CPU 중심의 작업을 메인 스레드에서 오프로드 할 때 사용
    • 공유 워커: 워커간 공유가 필요할 때 사용
    • 서비스 워커: 네트워크 요청을 캐싱하거나 오프라인 작업을 위해 사용
    • 워크렛: 브라우저 렌더링 파이프 라인에 연결되어, 스타일이나 레이아웃등 렌더링 프로세스에 저수준 접근이 가능

    와 같다.

     

    +.

    예전에 병렬 연산을 위해 WebCL이란 표준이 있었는데 지원하는 브라우저가 없는 모양.[WebCL]

     

    해결방법

    - 기초 사용방법

    가장 보편적인 전용 워커, 서비스 워커, 페인트 워크렛의 예제를 들어본다.

     

    전용 워커

    new Worker를 이용해 생성하며, postMessage와 onmessage로 통신한다.

    확장기능을 만들어본 사람은 background와 content의 통신방식과 유사해 적응이 쉬울것이다.

    /* main.js */
    
    // Create worker
    const myWorker = new Worker('worker.js');
    
    // Send message to worker
    myWorker.postMessage('Hello!');
    
    // Receive message from worker
    myWorker.onmessage = function(e) {
      console.log(e.data);
    }

     

    worker에서도 사용방법은 같다.

    /* worker.js */
    
    // Receive message from main file
    self.onmessage = function(e) {
      console.log(e.data);
    
      // Send message to main file
      self.postMessage(workerResult);
    }

     

    특히 Offscreen Canvas가 주목받을만 하다.[크로미움 엔진과 오프스크린 캔버스 기술, OffscreenCanvas — Speed up Your Canvas Operations with a Web Worker, Web Worker-Postable 라이브러리 작성기]

     

    서비스 워커

    방금전 워커 예제처럼 서비스 워커파일을 참조해서 등록해야 한다.

    /* main.js */
    
    navigator.serviceWorker.register('/service-worker.js');

     

    서비스 워커는 프록시의 역할을 하기 때문에 모든 네트워크 요청을 가로챌 수 있다.

    /* service-worker.js */
    
    // Install 
    self.addEventListener('install', function(event) {
        // ...
    });
    
    // Activate 
    self.addEventListener('activate', function(event) {
        // ...
    });
    
    // Listen for network requests from the main document
    self.addEventListener('fetch', function(event) {
        // ...
    });

     

    예를 들어 캐시에서 문서를 반환하도록 만들어 오프라인 상태로 작동시킬 수도 있다.

    PWA에 필수적인 요소니 꼭 알아두었으면 한다.[고성능 서비스 워커 로딩, Lazy load your images with Service Worker]

    /* service-worker.js */
    
    const cacheName = 'shell-content';
    const filesToCache = [
      '/css/styles.css',
      '/js/scripts.js',
      '/images/logo.svg',
    
      '/offline.html’,
    
      '/’,
    ];
    
    self.addEventListener('install', function(e) {
      console.log('[ServiceWorker] Install');
      e.waitUntil(
        caches.open(cacheName).then(function(cache) {
          console.log('[ServiceWorker] Caching app shell');
          return cache.addAll(filesToCache);
        })
      );
    });
    

     

    또는 이런식으로.

    /* service-worker.js */
    
    self.addEventListener('fetch', function(event) {
        // Return data from cache
        event.respondWith(
            caches.match(event.request);
        );
    });

     

    페인트 워크렛

    역시나 JS 파일을 등록한다.

    /* main.js */
    
    CSS.paintWorklet.addModule('myWorklet.js');

     

    그 후, 사용자 정의 이미지를 만드는 예. Canvas API와 유사하다.

    /* myWorklet.js */
    
    registerPaint('myGradient', class {
      paint(ctx, size, properties) {
        var gradient = ctx.createLinearGradient(0, 0, 0, size.height / 3);
    
        gradient.addColorStop(0, "black");
        gradient.addColorStop(0.7, "rgb(210, 210, 210)");
        gradient.addColorStop(0.8, "rgb(230, 230, 230)");
        gradient.addColorStop(1, "white");
    
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, size.width, size.height / 3);
      }
    });

     

    만든 사용자 정의 이미지는 CSS에서 적용하면 끝!!

    div {
        background-image: paint(myGradient);
    }

     

    - 라이브러리

    워커들이나 워크렛을 그냥 사용하기에는 불편한 점들이 많다.

    대표적인 라이브러리들을 소개해본다.

     

    웹 워커

    웹 워커를 이용하기 쉽게 만든 라이브러리

    • Comlink: 구글이 만든 것으로 잘 추상화 시켜놨다. [comlink-loader]
    • Greenlet:  async 함수를 웹워커로 처리
    • Workerize: 모듈을 웹워커로 처리
    • Workly: 역시 함수나 클래스를 워커로 처리도록 만든 것
    • Operative: 콜백 형식으로 처리하도록 만듦

     

    동시성이나 병렬 처리시 좋은 라이브러리.

     

    리엑트에서 사용하기 좋은, 또는 아이디어가 재미있는 것

     

    서비스 워커

     

    워크렛

    • extra css: 웹사이트 꾸미기에 좋은 Paint API 라이러리

     

    3.2.2 그래픽 최적화

    개요

    분류: Content, CSS, Javascript

    그래픽 최적화의 핵심은 GPU를 어떻게 써먹는가이다. [하드웨어 가속에 대한 이해와 적용, Animation performance and frame rate]

    하드웨어 가속은 Graphics Layer 단위로 처리되며, GPU를 이용해 이미지로 합성(Composition)해 화면에 출력한다.

    다음은 하드웨어 가속 대상이다.

     

    그러나 하드웨어 가속은 조심히 사용해야 하는데 3가지만 뽑아보았다.

    • 메모리: 많은 텍스처를 로드하면 메모리 문제가 발생할 수 있다.
      특히, 모바일 장치에서 문제가 생길 수 있으며, 모든 요소에 하드웨어 가속을 적용하면 안된다.
    • 깜박임: 영역이 크면 화면이 깜박일 수도 있다.
    • 글씨 렌더링: CPU와 CPU의 렌더링 방식으로 인해 안티얼라이싱에 영향을 주어 흐리게 표시될 수 있다. [GPU text rendering in webkit]

     

    해결방법

    웹에서 그래픽을 표현할 때 여러가지 방법이 존재한다. [A Comparison of Animation Technologies, Compare the options for Animations on the Web]

     

    CSS와 자바스크립트의 애니메이션에 대해 다음 문서에서 간략히 다루고 있다.[CSS와 자바스크립트 애니메이션CSS and JavaScript animation performance]

     

    정리하자면,

     

    CSS

    GPU 가속이 되도독 작성해야 한다.

    현재는 opacitytransform 정도만 지원. [AMP - 지원하는 CSS, Stick to Compositor-Only Properties and Manage Layer Count, Highperformacne Animations]

     

    다음과 같이 대체하여 사용할 수 있다.

     

    position을 absolute나 fixed로 설정하면 주변 영역에 영향을 주지 않는다.

     

    약간의 꼼수로 컴포지터 쓰레드로 레이어를 분리시켜 성능을 높일 수 있다.

    will-change를 사용하면 애니메이션이 곧 일어날 것이라고 알려줘 응답성을 개선할 수 있다.

    하지만, 우선순위를 변경하는 것이기 때문에 신중히 사용하고 해제해야 한다. [CSS will-change 프로퍼티에 관해 알아둬야 할 것]

    .moving-element {
      will-change: transform;
    }

     

    will-change가 먹히지 않는 브라우저라면, 강제로 적용 가능하다.

    .moving-element {
      transform: translateZ(0);
    }

    물론 CPU와 GPU의 대역폭, GPU의 메모리, 모바일의 하드웨어 가속여부 및 리소스 크기를 따져가며 작업해야 한다.

     

    나중에 시간이 나면 더 서술할 예정.

     

    3.2.3 웹 어셈블리

    개요

    분류: wasm

    웹 어셈블리는 네이티브에 가까운 성능으로 동작하는 바이너리 포맷을 제공하는 프로젝트로 C/C++, Rust 등의 컴파일 타겟으로 사용될 수 있다.

    나오게 된 계기는 WebAssembly, 브라우저에 올리는 네이티브 코드를 살펴보면 재미있게 읽을 수 있다.

    A cartoon intro to WebAssembly(번역)도 읽을만한 하구.

     

    웹브라우저에서 웹 어셈블리를 위한 컴파일러를 제공하는 것도 주목할만 하다.

     

    유니티에 따르면 상당히 성능이 좋다. [WebAssembly LoadTimes and Performance]

    메인 스크린에 보이는 총시간(낮은 것이 좋음)

    총점수(높은 것이 좋음)

     

     

    그렇다면, 웹 어셈블리의 성능은 왜 좋을까?

    • 웹 어셈블리 코드의 크기가 작음
    • 웹 어셈블리의 디코딩 시간이 자바스크립트 파싱 시간보다 적음
    • 이미 컴파일 되어 있는 상태라 최적화가 잘되어 있음
    • 타입이나 기타 정보가 있어 최적화에 유리
    • 명령어셋을 사용할 수 있음
    • 직접 메모리를 관리해 GC가 필요 없음

     

    써보려면 MDN 문서를 정독해보는 것이 좋다.

     

    물론 DOM에 접근할 수 없기 때문에 자바스크립트를 완전 대체하기보다 CPU 성능 집약적인 작업이나 기존 C/C++ 코드를 포팅해서 사용할 때 유용할 것이다.

    Dorioasm-dom처럼 Virtual DOM을 웹 어셈블리로 하는 사례도 있는 듯.

     

    3.2.4 전용 페이지

    개요

    분류: Content

    컨텐츠를 전용 페이지로 만들어 제공하면 빠르고 로드 할 수 있다.[Instant loading on mobile: should you AMP-ify / MIP-ify your website in 2018?, Facebook Instant Articles vs. Google AMP, AMP and Turbo Pages: Pros, Cons, and Implementation Results, What do Facebook Instant Articles, Apple News & Google AMP mean for publishers?]

     

    처음에는 컨텐츠를 일정한 UI로 만들어 통일성을 만들고, 광고 수익을 챙기고, 접속율을 늘리려는 과정에서 다음과 같은 것들이 등장하였다.

    이 중 유의미한 성능과 관련된 결과를 보여주는 인스턴트 아티클을 살펴보자면

    • 사이트의 CSS, Javascript, 광고 및 분석 등을 아낄 수 있음
    • 복잡한 레이아웃, 인터페이스 렌더링 필요 없음
    • 페이스북에서 사진, 비디오를 빠르게 로드하는 것과 동일한 기술
    • 스토리에 접근할 때 스토리를 미리로드 시작

     

    애플의 경우, 이미지 캐싱과 JSON 포맷이라 가벼워 성능에서 유리한 편.

     

    폐쇄적인 플랫폼에서만 쓰였던 컨셉이 열려있는 플랫폼(검색엔진)에도 적용되기 시작했다.

    AMP와 MIP는 동일한 기술.

     

    AMP가 성능을 높힐 수 있었던 주요 이유는 다음과 같다. [AMP는 어떻게 웹 페이지의 성능을 높일 수 있나]

    • AMP HTML: 커스텀 엘리먼트를 사용하여 레이아웃 계산 최소화
    • AMP JS: 리소스 로딩 관리(비동기, 느린 것 차단) 및 커스텀 엘리먼트 제어.
    • AMP Cache: 프록시 기반 CDN으로 캐시를 이용, 모든 스크립트와 이미지를 같은 출처(Origin)에서 다운 가능
    • 제한: 서드파티 CSS, JS 미허용 및 용량 제한, GPU 가속 애니메이션만 허용, 상태 및 이벤트 관리

    이 페이지에서 나오는 것들이 빡시게 적용되었다는 것을 알 수 있다.

     

    Turbo Pages의 컨셉을 살펴보면 AMP와 Instant Page 사이.

     

    3.3 UX 트릭

    객관적인 성능이 올라가는 것이 아니라 사용자가 빠르다고 느낄 수 있게 만드는 트릭들.

    UX는 글로만 설명하기 힘든 부분이 많기 때문에 가급적 GIF나 사진자료 통해 직관적으로 이해할 수 있도록 힘을 썼다.

     

    초반에 나오는 것들은 많이들 접해봤겠지만, 후반에 나오는 낙관적 UI(Optimistic UI)와 피츠의 법칙 활용은 일반 개발자에게 상당히 생소한 존재며 흥미로운 방법론들이 많다.

    UX에 관심이 있는 사람이 아닐지라도 인사이트를 얻어갈 수 있을 것이다.

     

    3.3.1 웹폰트 로드

    개요

    분류: Font

    앞선 폰트 압축과 서브셋에 이어 로드 최적화를 다루어본다. [웹 폰트 사용과 최적화의 최근 동향, 웹폰트 최적화, 웹폰트 사용하기 (웹폰트 101), A Comprehensive Guide to Font Loading Strategies, Faster Font Loading with Font Events, FOUT, FOIT, FOFT, Optimizing Web Font Rendering Performance, 웹폰트 최적화 기법에 관한 몇가지 이야기]

     

    - 렌더링 차단방식

    폰트 렌더링 방식은

    • FOIT(Flash Of Invisible Text): 텍스트가 보이지 않은 상태(invisible)에서 지정된 폰트로 바뀌며 번쩍임 (크롬, 파이어폭스)
    • FOUT(Flash Of Unstyled Text): Fallback 폰트(unstyled)에서 지정된 폰트로 바뀌며 번쩍임 (IE)

    으로 나뉘어져 있다.

     

    이중 컨텐츠를 먼저 보여는 FOUT 방식이 UX에 좋다.

    그러나 글꼴의 자간, 높이 등 서식 때문에 레이아웃이 변경될 수 있다.

     

    - FOUT 사용

    font-display 속성

    요즘 브라우저들은 font-display를 적용하여 FOUT처럼 사용할 수도 있다.

    • auto: block과 비슷
    • block: FOIT처럼 작동, 로딩되지 않을 때는 텍스트를 렌더링하지 않으며(최대 3초) 웹폰트가 로딩되면 적용
    • swap: FOUT처럼 작동, 우선 Fallback으로 렌더링하고 웹폰트가 로딩되면 적용
    • fallback: 100ms 동안 렌더링하지 않다가 Fallback으로 렌더링, 3초안에 로딩이 완료되면 완료된 폰트로 아니라면 Fallback 폰트 유지
    • optional: 100ms 동안 렌더링 하지 않다가 Fallback으로 렌더링, 브라우저가 네트워크 상태를 파악해 웹폰트로 전환여부를 결정

     

    @font-face {
      font-family: 'Awesome Font';
      font-display: swap; /* or auto, block, fallback, optional */
      src: local('Awesome Font'),
        url('/fonts/awesome-l.woff2') format('woff2');
    }

    @font-face의 font-display를 swap으로 설정하면 된다.

     

     

    자바스크립트 라이브러리

    FOUT 방식으로 로드하도록 도와주는 라이브러리가 역시 존재한다.

    이 중에서는 Font Face Observer가 가볍고 성능에서도 낫다고 알려져 있다.

    현재 메인테이너가 참여를 안하는 중인데..

     

    풀리퀘스트를 날린 사람이 만든 Font Face Observer ES를 사용하면 트리 쉐이킹을 사용할 수 있으니 요것을 쓰는게 좋을 듯 하다.

    Font Face Observer의 사용방법은 간단하다.

     

    Font Loading API

    최근에는 아예 Font Loding API가 나왔는데 Font Face Observer와 비슷하다. [Getting started with CSS Font Loading, CSS Font Loading API's FontFaceSet Sample]

     

    대략 다음같이 사용할 수 있다.

    var font = new FontFace("Awesome Font", "url(/fonts/awesome.woff2)", {
      style: 'normal', unicodeRange: 'U+000-5FF', weight: '400'
    });
    
    // don't wait for the render tree, initiate an immediate fetch!
    font.load().then(function() {
      document.fonts.add(font); // add font to document
      document.body.classList.add('fonts-loaded'); // enable font with CSS class
    });
    

     

    - 부작용 보완

    그러나 FOUT도 부작용이 존재한다고 하였다.

    대표적인 것이 서식에 의한 레이아웃 변경, 그리고 번쩍임이다.

     

    비슷하게 만들기

     Font style matcher를 사용해 비슷하게 보이도록 맞추어보자.

     

    그 후, 스타일을 지정해주면 끝.

     

    FOUT 최소화

    FOUT이 UX에 좋기는 하나 깜박이는 것은 좋지 않다.

    따라서 작은 시간내에 로드가 된다면 깜박이지 않게 만드는 것을 고려할만 하다.

     

    400ms 이내에 전송이 되면 깜빡임 없이 바로 보이게 할 수도 있다.

    .fonts-loaded body {
         font-family: 'Nanum Gothic', sans-serif, Lucida Sans Unicode, arial;
    }
    
    .blocking-time {
        opacity: 0;
    }
    
    .fonts-loaded.blocking-time {
        opacity: 1;
    }
    document.documentElement.classList.add('blocking-time');
    setTimeout(function() {
        document.documentElement.classList.remove('blocking-time');
    }, 400)
    var font = new FontFaceObserver('Nanum Gothic');
    font.load(null, 3000).then(function() {
      document.documentElement.classList.add('fonts-loaded');
    });

     

    FOFT(Flash of Faux Text)

    모든 웹폰트가 다 다운로드 받은 후, 로드 할 필요는 없다.

    만약 관련된 폰트가 먼저 다운로드 받아졌을 경우, 먼저 적용해보는 것도 좋은 방식

     

    예: 시스템 폰트 - 웹폰트 Regular - 웹폰트 Bold/Italic

    시스템 폰트(FOUT)

    먼저 다운로드된 폰트 적용(FOFT)

    모든 폰트 적용

     

    3.3.2 스켈레톤 스크린

    개요

    분류: Content, CSS

    스켈레톤 스크린(Skeleton Screen) 또는 스켈레톤 UI라 부르는 것은 로딩이 조금 더 빠르다고 느끼게 한다. [How to Speed Up Your UX with Skeleton Screens, Improve UX in React apps by showing skeleton UI, Everything you need to know about skeleton screens]

     

    컨텐츠가 로딩될 때까지 둘 중 어느 것처럼 보이는 것이 나을지 생각해보자.

     

    유튜브나 인스타그램 등에서 빈번히 쓰여서 알겠지만 빠르다는 느낌을 준다.

     

     

    해결방법

    CSS로 제작 할 수 있는 가이드.

     

    CSS로 만들 때 참고할만한 라이브러리, 생성기

     

    리엑트를 사용한다면 아주 좋은 라이브러리들이 존재한다.

     

    특히 React Content Loader는 Create React Content Loader에서 50가지가 넘는 예제와 다양한 옵션으로 스켈레톤 스크린 코드를 만들어낼 수 있다!!

     

    UI 참고용으로 다음 것도 한번 볼만 하다.

     

    3.3.3 이미지 자리 표시자

    개요

    분류: Content

    이미지 Lazy Load시 생기는 빈공간에 쓸 방법이 몇가지 있다.[How to use SVG as a Placeholder, and Other Image Loading Techniques(medium), How to create a kick-ass image preview with LQIP]

     

    • 빈공간: 자리를 채우지 않고, 그대로 나둠
    • 자리 표시자: 빈자리를 채울 때 사용할 만한 아이콘
    • 색 채우기: 이미지를 대표하는 색을 이용해 채우기
    • 가벼운 이미지: 가볍게 만들어진 이미지로 채우기

     

    빈공간은 UX에서 나쁘며, 매번 레이아웃 계산을 해야 하기 때문에 성능에 악영향을 끼친다.

    이하 다른 것들의 장단점과 몇가지 변형을 다루어보고자 한다.

     

    - 자리 표시자(Place Holder)

    자리표시자라는 이미지 요청을 실패했거나, 기본 아이콘으로도 사용할 수 있어 데이터가 더 필요하지 않으면서 UX를 향상시킬 수 있다.

     

    자리 표시자로 사용할 만한 것은

    Placeholder Image & Icons Image

    • 자리표시자 이미지
    • 해당 자리를 나타내는 기본 이미지

     

    자리표시자를 자동적으로 생성해주는 서비스도 있다.[21 of Best Placeholder Image Generator]

    이 서비스들 중 하나를 골라 사용하면, 자리표시자 이미지를 동적으로 생성해서 사용하는 것이 가능하다.

     

    예를 들어

    <img src="https://dummyimage.com/300x200/000/fff.png">

    는 다음 이미지다.

     

    - 색 채우기

    상징하는 색(Dominant Color)을 채운 후, 전환하는 예도 있다.[Dominant Colors for Lazy-Loading Images, How Medium does progressive image loading]

    전환이 부드럽고, 직관적이며 작기 때문에 트래픽 비용도 크지 않다.

    단색

    그라디언트

     

    색은

    • 단색
    • 그라디언트

    로 나뉠 수 있는데, 아무래도 그라디언트에서 용량이 살짝 커지는 편.

     

    단점이라면 이미지 프로세싱을 통해 색을 뽑아내야 한다는 것.

    인공지능 시간에 보던 k-means clustering을 사용하거나, 색상 양자화를 하면 이미지를 대표하는 색을 만들 수 있다.

     

    그 후 base 인코딩으로 내장하면 끝

    <img src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="
         data-src="https://s-media-cache-ak0.pinimg.com/474x/50/1b/74/501b74902935b063816ea8e14f460ca0.jpg"
         alt="Ghost In The Shell">

     

    CSS로 그라디언트를 만드는 것은 Gradify(깃허브)에서 확인 가능.

     

    - 가벼운 이미지

    역시 전환이 자연스럽기 때문에 사람들은 빠르다고 느낄 수 있다.

     

    실루엣

     

    손실된 저용량 이미지

     

    실루엣을 그리기 위해서는 contour나 node-potrace을 참고하면 된다. [Drawing images using edge detection and SVG animation]

     

    LQIP(Low Quality Images Placeholder)SQIP(SVG-based LQIP)를 사용해 블러, 모자이크, 폴리곤 형태의 이미지를 만들 수 있다.

    SQIP의 art는 마치 인상파와 야수파를 섞은 종이소재 작품 같다. ㅎㅎ

     

    웹팩의 경우, 다음을 사용해 편히 사용할 수 있다.

    +.

    CSSConfRender Conf처럼 들로네 삼각 분할 (Delaunay Triangulation)을 이용해 삼각 폴리곤처럼 만든 polyserver도 있다.

     

     

    - 비디오

    비디오의 경우 poster 속성을 사용해 재생 전까지 이미지를 보여줄 수 있다.

     

    3.3.4 로딩 인디케이터와 인터렉션

    개요

    분류: Content

    적절한 로딩 인디케이터와 인터렉션은 사용자에게 빠르다고 느끼게 만들 수 있다.[XD 필수 항목: 애니메이션 진행률 표시기의 모범 사례, Everything you need to know about Loading Animations, Improving the UX of Progress Indicators and Feedback Notifications, 모바일 앱에서의 대표적인 애니메이션 활용 방법]

    시스템 상태의 가시성은 유저 인터페이스 디자인에서 가장 중요한 원칙 중 하나입니다. 사용자는 사용 중인 시스템에 대해 알고 싶어하는데, 특히 시스템이 한창 가동 중일 때 언제든지 현재 상황을 파악하여 시스템 상태를 파악하길 원합니다. 대기 상태의 애니메이션 진행률 표시기는 작업이 진행 중이거나 로딩되고 있을 때 사용자에게 시스템 상태를 알리는 가장 일반적인 방법입니다.

    현재 웹에서 애니메이션이 빈번히 사용되는 것은 아니나 점점 APP화가 되어가는 추세이기 때문에(PWA, SPA등) 나중에는 인터렉션의 중요성이 부각될 것이라 생각한다.

     

    로딩 인디케이터

    로딩 인디케이터는 다음의 장점을 가진다.

    • 시스템이 충돌하지 않고, 계속 처리시킴을 알려 안심시킴
    • 사용자가 얼마나 기다려야 하는지 예상가능
    • 사용자가 볼 수 있도록 시각적인 것을 제공

     

    로딩 인디케이터에는 크게 프로그레스 바와 스피너가 있는데 다음과 같이 써야 한다. [Progress Bars vs. Spinners: When to Use Which]

    • 4초이상일 때: 프로그레스바, 아니라면 스피너
    • 1초 미만일 때: 아무것도 사용하지 말기
    • 멈추어 있으면 않됨

    프로그레스 바와 스피너

     

    애니메이션과 인터렉션

    다음글에서 설명이 매우 잘 나와있어 따로 안해도 될 듯. [[번역]UX에서 적절한 애니메이션효과를 위한 가이드 (2편), 모션으로 사용성 만들기 : 모션 선언문의 UX (2편), ease-out, in; ease-in, out]

     

    해결방법

    로딩 인디케이터

    로딩 인디케이터는 오래 걸리는 작업에만(약 5초 이상) 사용해야한다. [Stop Using A Loading Spinner, There’s Something Better, Mobile Design Details: Avoid The Spinner]

    성능이 좋을 때 로딩 인디케이터를 사용하면, 잠깐 비쳤다 사라져 불안감을 느끼게 하고 기다려야 한다는 사실을 인식하여 느리다고 느낀다.

    시계가 똑딱 거리는 것을 보는 것과 마찬가지.

    • 성능을 향상 시키는 것이 가장 좋은 방법
    • 화면에 잠깐 나오는 것은 좋지 않음
    • 전환 애니메이션이나 스켈레톤 스크린을 사용하여 감추는 것을 고려

     

     

    로딩 인디케이터는 사용자에게 충분한 정보를 직접적/간접적으로 제공해야 한다.

    • 왜 로딩 인디케이터가 표시되고 있는지
    • 오래 걸린다면 얼마나 걸리는지
      • 이때, 인식되는 값을 대기 시간과 일치 시켜야 한다.
    • 어떤 작업을 하고 있는지

     

    재밌거나 창의적인 로딩 인디케이터는 사용자의 인식을 분산시킬 수 있다.

    • 눈에 띄는 색상 조합
    • 흥미롭거나 귀여운 아이디어
    • 브랜드 이미지 부여
    • 각종 팁이나 정보 보여주기

    정보를 보여주는 것은 게임에서 자주 쓰이는 방식. [pull to refresh airbnb, Case study #01 배달의 민족배민키친 로딩, Any one else stuck on creative loading screen?]

     

     

    네이티브나 간단한 로딩 인디케이터를 사용하는 것도 좋다.

    • 개발 시간 단축(개발자, 디자이너 모두)
    • 처리 리소스를 아낌
    • 사용자는 앱 성능 대신 인터넷 연결 또는 장치 속도에 대해 불평 할 가능성이 높음

    네이티브 로딩 스피너 느낌[Mobile Loading Spinners Sketch Resource, SVProgressHUD, Native iOS and Android loading spinner (progress bar indicator) overlay]

    개인적으로는 2번째 로딩 스피너를 좋아한다.

    ios, android, 웹에 다 적합한 느낌.

     

    마지막으로 로드 인디케이터 동작에 대한 이야기. [Faster Progress Bars: Manipulating Perceived Duration with Visual AugmentationsDoes Progress Bars’ Behavior Influence the User Experience in Human-Computer Interaction?, 진행바 속도가 빠르다고 느끼게 만드는 방법, '로딩바 - 프로그레스바'의 비밀]

    • 프로그래스바의 강조를 자주, 스피너라면 회전수를 늘림 
    • 세로무늬가 거꾸로 진행하는 것이 더 빠르다 느낌
    • 진행 마지막에 가속화(처음에 느리게, 마지막에 빠르게)
    마지막의 가운데 두개가 가장 빠르다고 느낌

     

    애니메이션과 인터렉션

    동작 속도

    • 100~350ms 정도
    • 리스트 아이템은 250ms 이하로
    • 이동 시간 때문에 화면이 작은 기기에서는 빠르고, 큰 기기에서는 느리게
    • 작은 요소는 빠르고, 큰 요소는 느리게

     

    동작 가속 [비대칭 애니메이션 타이밍, 사용자설정 easing]

    • 바운스 효과는 제거
    • 명확하고 날카롭게
    • 이징은 항상 사용(선형 동작 보다 가속을 붙혀서)
    • Ease-in(가속)은 사라질때, Ease-out(감속)은 나타날 때
    • Ease-in-out(표준곡선, 가속 후 감속) 시 비대칭으로

    Ease-in-out 효과를 사용시 애니메이션 곡선도구를 활용해보자.

    애니메이션 곡선도구

     

    연출

    • 같은 컨테이너의 요소는 동일한 방향으로
      • 표의 경우 대각선으로
    • 같은 컨테이너에서 가능한 적은 요소에만 적용
    • 같은 컨테이너의 요소는 Offset(상쇄, 원래 상태로), Separateness(분리) 되도록
    • 크기 변화가 균일하면 직선, 불균일하면 커브로 이동
      • 커브로 동작시, 스크롤 인터페이스 주축(보통 Virtical out)과 일치해야 함
    • 계층화 되었을 시 공간적인 관계를 가짐
      • 움직이는 물체끼리는 통과하지 않도록
      • 떠오르게 만들시에는 가능
    • 상호작용시 공간적/시간적 계층관계 만들기
    • 사용성이 변경 될 때는 내러티브(이야기, 맥락, 서술)에 맞는 연속적인 상태로
      • 문자와 숫자 값은 동적으로 변경
      • 보이거나 숨겨질 때 연속적으로
      • 현재화면에서 새로운 오브젝트가 생성시 Cloning
      • 새로운 오브젝트 생성시 차원 이용
      • 모호한 동작시 정적인 상태에서 일시적으로 움직임
      • 스크롤 시 2차원적인 공간에 계층구조
      • 오브젝트, 공간을 탐색시 연속적이고 공간적이게

     

     

    여기 이쁘고, 좋은 예들이 존재한다. [다음웹툰의 UX 개편 이야기, UX Design: How to Use Animations in Mobile Apps]

     

    홈 화면 로딩, 탭전환, 리스트 로딩, 페이지 전환

     

     

    3.3.5 낙관적 UI

    개요

    분류: Content

    낙관적 UI(Optimistic UI)는 작동하는 척 일종의 사기(?, 그냥 블러핑이라 해두자)를 치는 것 이다. [옵티미스틱 UI의 계산된 거짓말, Optimistic UIs in under 1000 words, Being optimistic in UI, The 3 White Lies Behind Instagram’s Lightning Speed, Secrets to Lightning Fast Mobile Design, 3 Tips To Make Your Application Feel Faster]

     

    먼저 작동완료 된 것처럼 표시하고, 나중에 업데이트를 하자는 뜻인데, 절차는 다음과 같다.

    기존 방식과 낙관적 UI

    • 이벤트 발생
    • 성공한 것처럼 상태(로컬) 업데이트
    • 요청 시작
    • 오류가 생겼는지 파악, 생겼다면 전 상태로 롤백[기준은 보통 2초]

     

    바로 전에 나왔던 애니메이션 효과를 이용한 방법도 동작하고 있는 것처럼 시간(약 100~350ms)원리이니 일종의 Optimistic UI라 할 수 있겠다.

    더 전의 스켈레톤 스크린도 그렇고.

     

    해결방법

    예시 및 장점

    우선 얼마나 유용한지 예제를 보자.

    일반 vs Optismitic UI

     

    간단한 데모예제로는 이런 느낌 정도?

     

    특히 메세징에 유용하다.

    먼저 보낸척 표시하고 작은 로딩바가 표시되는 것을 확인가능하다.

    맥 iMessage와 인스타그램

     

    인스타그램의 좋아요 표시, Audible의 다운로드 받으며 듣기(스트리밍)도 비슷한 예제.

     

    마지막으로 트렐로 예인데, 웹에서 일반앱급 느낌을 내게 만들어준다.

    UI와 서버상태를 비동기로 운영한다고 봐도 무방.

     

    페이스북이나 인스타그램에서 잘 써먹고 있는 기법이라 그런지 잘 짜여진 코드 예제들도 많다.

    그리고

    이 정말 편해보인다.

     

    단점 보완

    단점은 업데이트중인가, 오류가 났는가를 파악하기 힘들다는 것이다.

    따라서 이것역시 UI상으로 표시를 해줘야 한다.

     

    업데이트시에는 로딩 인디케이터나 상태 아이콘을 표시해주고

    오류시에는 모달창으로 띄우거나 실패했다는 메세지를 띄울 수 있다. (UX상 후자가 좋음)

     

    사용자가 앱을 꺼서 파악을 못할 경우엔

    아이콘에 경고표시를 해주면 된다.

     

    3.3.6 피츠의 법칙과 UI/UX 개선

    개요

    분류: Content, JS

    우선 알아두어야 할 것은 피츠의 법칙(Fitts's Law)이다. [The Glossary of Human Computer Interaction, 피츠의 법칙]

    아주 건단하게 설명하자면, 목표지점으로 빠르게 이동하는데 시간이 거리와 목표 폭 사이의 비율에 관한 것이다.

    목표B에서 허용하는 너비 W와 거리 D

    당연하게 느껴질 수도 있겠지만

    • 너비 W는 크게
    • 거리 D는 작게

    만드는 것이 좋다.

     

    해결방법

    피츠의 법칙에 대한 이해를 돕기위한 퀴즈가 존재한다. [A Quiz Designed to Give You Fitts(번역)]

    이건 정말 원문을 보기 권한다.

     

    오래된 글이라 PC위주의 팁이지만, 잘 사용한다면 웹에서도 충분히 활용가능하다.

    적어도 내눈에는 그리 보인다.

     

    - 레이블과 툴바

    레이블은 버튼의 일부가 되어 너비가 커지므로 더 빠르게 접근 가능하다.

    레이블이 없으면 버튼이 모이게 되는데, 원치않는 버튼을 누르는 것을 막기위해 다가갈 때 속도를 늦추게된다.

    Windows32 Toolbars

     

    MacOS Toolbars

     

    - 줄배열과 가장자리 배치

    배열을 여러줄이 아니라 한줄이고, 가장자리와 맞닿아 있아면 빠른 접근 가능하다.

    한줄로 만들면 방금 언급했던 것처럼 원치않는 버튼을 누르는 것을 막기위해 속도를 늦추는 행위가 사라진다.

    여러줄은 한줄로[10 ESSENTIAL TOOLS & TIPS ALL PHOTOSHOP BEGINNERS SHOULD LEARN]

     

    가장자리에 배치하게되면 속도를 늦출이유가 없어 빠르게 누를 수 있다.

    가장자리와 맞닿아있게[Reattaching The Toolbar, Perspective Crop Tool Missing]

     

    - 가장 빨리 접근 가능한 위치

    오른손잡이 기준, 현재위치, 우측하단, 좌측상단, 우측상단, 좌측하단으로 접근이 빠르다.

     

    어찌보면 당연하겠지만, 현재자리에서 바로 누를 수 있는 것에 접근이 빠르다.

    현재자리에서 바로 부를수 있는 대표적인 메뉴가 바로 컨텍스트 메뉴.

    현재 위치의 컨텍스트 메뉴(오른쪽 클릭)

     

    사람의 팔 구조상 호를 그리며 움직이게 되는데, 때문에 아무렇게나 움직이다보면 네 귀퉁이에 닿게된다.

    이를 이용한 것이 바로 윈도우 시작버튼과 KDE의 화면경계 기능.

    윈도우 시작버튼과 KDE 화면경계 기능

    역시 구조상의 이유로 우측하단과 좌측상단, 우측상단과 좌측하단순으로 접근성이 높다.

     

     

    잘만들어졌다고 칭찬받는 삼성 One UI도 이 원칙하에 있다.

    패블릿이라 불릴정도로 크기가 커진 스마트폰은 상단에 손이 닿기가 어렵고, 좌우로 접히는 폴더블과 원형태의 스마트워치는 가운데를 터치하기가 어렵다.

    때문에 아래 사진들을 보면 하단과 가장자리 위주로 UI가 구성되어있음을 알 수 있다.

    삼성 One UI [One UI 2]

     

    - 작업표시줄의 자동숨김

    작업표시줄 자동숨김은 비효율적이다.

     

    계속 말해왔지만 화면 가장자리는 접근성이 매우 좋다.

    고정된 작업표시줄을 사용하면 수많은 아이콘에 접근할 수 있지만, 자동숨김처리된 작업표시줄을 사용하면 작업표시줄 하나를 접근하기위해 시간을 낭비해야 한다.

    또한, 화면 가장자리에 있는 요소를 선택하려할 때 작업표시줄이 나타나 원하는 작업을 못하는 경우가 생긴다.

    작업표시줄 헤멤 [How To Hide Windows 10 Taskbar Using A Hotkey]

     

    - 프로그램 드롭다운 메뉴위치

    프로그램창 내부보다 디스플레이 상단에 메뉴를 배치한 것이 더 빠른 접근 가능하다.

     

    이유는 역시 화면 가장자리라 접근이 빠르기 때문.

    항상 같은 자리에 있을 것이라는 것을 알며, 화면크기가 작을때도 접근하기 좋다.

    프로그램 창의 메뉴와 디스플레이 상단 메뉴

    - 드롭다운과 탈출각도

    메뉴를 누르려고 할 때 각도가 좁으면 실수로 다른 항목을 누르기 쉽다.[Making Menus Escapable]

    각도가 좁으면 객체의 너비가 줄어들기 때문.

    OS X: 65도, Ubuntu: 50도

    또한, 사용자는 텍스트를 대상으로 생각하고 움직일 것이기 때문에 탈출각도와 완전히 틀어진 우분투의 텍스트배치는 실수를 유발한다.

     

    으아.. 화가난다..[Ubuntu Volume Indicator Applet - Maverick (Terrible Design!)]

     

    이를 보완할 몇 가지 방법을 소개한다.

     

    크기 조절

    메뉴의 크기를 넓게 만들면 탈출각도가 넓어진다.

    하지만, 실제로 메뉴를 탈출하기는 어려울 수도 있다는 점은 고려대상.

     

    시간과 공간의 여유

    윈도우는 메뉴가 사라져버리는 것을 10~100ms처럼 여유시간을 두는 방식으로 해결한다. [Hysteresis]

     

    그러나 애플은 탈출각도란 개념을 이용해 직선으로 갈 수 있게 만든다.

    무려 80년대(정확히 1986년)에 구현했었다고. [Mac OS X DP2, Hierarchical Menus]

     

    그 후, NeXT와 OS X 초기 팀이 복제에 실패해 잊혀졌다가 더 나은 방식으로 왕귀했다.

    무조건 오른쪽의 부채꼴을 유지하는 것이 아니라, 메뉴의 크기를 고려하는 것이다.

     

    추후에 등장할 파트에서 이를 이용해 응답성을 극도로 높힌 흑마법을 선보일텐데 이론적 기반없이 처음 접했다면 감탄을 연발할 것이다.

     

    클릭할 때까지 활성화

    메뉴나 다른 공간을 클릭할 때까지 계속 활성화 시키는 방법도 있는데, 이 경우 갈수있는 모든 각도가 탈출각도가 된다.

    하지만 어딘가를 무조건 클릭해야 한다는 것은 커다란 단점.

     

    - 원형메뉴

    원형 팝업 메뉴는 선형 팝업메뉴보다 접근하기 쉬움

     

    원 주변 조각으로 움직이기 위해 조그만 움직여도 되며, 방향정보를 기억할 수 있기 때문이다.

    선형 메뉴 vs 원형 메뉴[New in Drawboard PDF: The Advanced Tools Drawer]

     

     

    - 선형 팝업 메뉴의 모든 옵션에 접근시간 균일화

    옵션의 크기 가속화

    포인터에서 멀수록 옵션의 크기를 강하게 주면 된다.

    예를 들어 포인터를 멀리 이동시킬수록 더욱 많은 양을 움직이는 식.

     

    모바일에서도 비슷한 방식이 관성 스크롤이란 방식으로 적용되어있다.

    쎄게(짧고 빠르게) 스크롤하면 약하게(길고 느리게) 스크롤하는 것보다 훨씬 멀리 움직이지 않은가? 

     

    관성 스크롤

    ios의 경우 overflow-scrolling을 쓰면 조절이 가능하다.

    .scroll-touch {
      -webkit-overflow-scrolling: touch; /* Lets it scroll lazy */
    }
    
    .scroll-auto {
      -webkit-overflow-scrolling: auto; /* Stops scrolling immediately */
    }

     

    ios에서 지원하는거라 변태 개발자들이 포팅이나 폴리필을 만들었을까봐 안드로이드나 기타 환경에서도 가능한지 여러가지를 찾아봤는데 아직 딱히 마음에 드는 것을 찾지 못했지만, 다음글은 참고할만 하다.[Recreating native iOS scroll and momentum, [번역]iOS10의 프리-패칭 API로 부드러운 스크롤 증진하기, [번역] 미래를 향한 스크롤 (Scroll to the future), Scrolling personality improvements in Microsoft Edge]

     

    빨아들이기

    객체에 일종의 중력장이 만들어지면 빨려들어가게 만들면 빨리 움직이더라도(부정확하더라도) 멀리있는 버튼에 접근하기 쉽다.

    마치 에임핵처럼(Aim hack?)

     

    모바일에서 비슷한 것을 찾으라면 패스트 스크롤(Fast Scroll)이나 사이드 인덱스바(Side Index Bar)라 불리는 물건.

    각 인덱스의 위치가 중력처럼 작용하여 빠르게 이동할 수 있다.

    Fast Scroll [Contacts XT - Address Book]

     

    가운데로 위치

    하위메뉴들의 중간부분이 계층에서 이동할 수 있게 만든다면, 비교적 균일하게 접근이 가능하다.

    중앙에 중요한 메뉴가 위치한다면 팝업이나 컨텍스트 메뉴에 좋은 방법.

    평행하게 vs 중앙으로

     

    3.3.7 기타 응답성 개선

    개요

    분류: Content

    이 파트에서는 기타 응답성(Respose time) 개선하는 방법들을 서술한다. [How to create the illusion of faster web pages (while also creating actual happier users)]

    앞서 나왔던 애니메이션이 일어날 것이라는 예정을 알려주는 CSS의 will-change 속성, 터치나 포인터 이벤트로 300ms 지연속도를 없애는 것은 대표적인 응답성 개선 방법이다.

    여기에서는 기타 응답성 개선 방안들을 다룬다.

     

     

    - 터치 응답성

    우선 중복된 부분으로는 이벤트와 API에서 다루었던 버튼에 터치 상태를 추가하여 300ms의 지연속도를 없애버리는 게 있다. [사이트에 터치 추가]

     

     

    - 스크롤링 응답성

    최근 브라우저들은 파이어폭스의 Async Pan/Zoom(APZ)처럼 Off-thread 스크롤링을 지원해  메인 쓰레드가 막히더라도 스크롤링을 지원한다. [Async Pan/Zoom (APZ) lands in Firefox Quantum, Scrolling on the web: A primer]

    apz

     

    원리를 대충 설명하자면, 입력을 받는 쓰레드를 따로 분리해 컴포지터 쓰레드로 전달이 되도록 하여 병목이 일어나더라도 비동기적으로 스크롤을 할 수 있는 것.

     

    이 기술의 파워를 알 수 있는 데모를 2가지 있다.

    Off-thread 스크롤링을 지원하는 브라우저라면

    첫번째 페이지의 "Junk me!"를 누르고 스크롤링을 하거나 두번째 페이지에서 그냥 스크롤링을 할 때 부드러움을 느낄 수 있다.

    그러나 Off-thread 스크롤링을 지원하지 않은 키보드 입력의 방향기를 사용하면 작동이 안되거나(1번째), 끊김이 심함(2번째)을 알수 있다.

     

    이 항목에서는 메인 쓰레드를 막지 않으면서 스크롤이 가능한 방법을 알아보자.

     

    CSS 활용

    계속 강조해오는 것이지만, 애니메이션이나 레이아웃들과 관련해서 CSS를 우선적으로 사용하는 것이 좋다.

    여기서는 모질라에서 공개한 문서에서 알려주는 예제를 기반으로 살펴도록 하자. [Scroll-linked effects, Asynchronous scrolling in Firefox]

     

    scroll-behavior를 smooth로 설정하면 항목끼리 부드럽게 움직일 수 있다.

    앞서 Scroll-Behavior의 Scroll up과 Scroll down 버튼을 눌러보면 알겠지만, 위아래로 부드럽게 움직인다.

     

    두번째는 posion을 어느 상한선에서 고정시키는 sticky.

    밑의 예제에서 스크롤시 Javascipt로 제작한 것은 깜박이고, 버벅이고 난리를 치나 CSS는 고고하게 자리를 지킨다.

     

    CSS Scroll Snap을 이용하면 CSS만으로 한화면씩 전환하는 테크닉을 만들 수 있다.  [Practical CSS Scroll Snapping, Well-Controlled Scrolling with CSS Scroll Snap]

     

    마지막으로 시차(parallax)를 두고 스크롤링을 하는 것인데 화려한 사이트에서 곧잘 볼 수 있다. [Practical CSS Parallax, CSS 로만 구현한 Parallax Scroll 원리 메모]

    구현이 살짝 복잡해지기 때문에 위 사이트들을 확인바란다.

    CSS mix-blend-mode & Awesome parallax scrolling, Landscape parallax using CSS

    링크의 코드들이나 리스트에 있는 링크의 Debug 버튼을 눌러보면 원리를 이해하는데 커다란 도움이 되리라 확신한다.

     

    이벤트 위임

    이벤트를 위임하여 처리할 때 event.preventDefault와 같은 패턴이 들어갈 수 있다. [event.preventDefault()를 활용한 이벤트 취소 방법, JavaScript에서 이벤트 전파를 중단하는 네가지 방법]

    그러나 body 영역 모두에서 이벤트가 취소가 되버리는 불상사가 일어날 수 있다.

    document.body.addEventListener('touchstart', event => {  
        if (event.target === area) {
            event.preventDefault();
        }
    });

     

    Scroll jank due to touch/wheel handlers demo

     

    이는 passive 속성을 true로 바꾸면 preventDefault를 호출하지 않아 성능개선이 가능하다.  [Improving scrolling performance with passive listeners, 스크롤 성능 개선을 위해 패시브 이벤트 리스너를 사용하는 사이트, { passive:true } 의 진정한 의미]

    이벤트 핸들러의 실행이 완료될 때까지 기다리지 않고 계속 스크롤을 할 수 있는 것이다.

    document.body.addEventListener('touchstart', event => {  
        if (event.target === area) {
            event.preventDefault()
        }
     }, {passive: true});

     

    응답성이 나아진 것을 확인 가능하다.

     

    만약 preventDefault를 쓰고 싶다면 event.cancelable로 취소가 가능한지 확인해보거나

    document.body.addEventListener('pointermove', event => {  
        if (event.cancelable) {
            event.preventDefault(); // block the native scroll
            /*
            *  do what you want the application to do here
            */
        }
    }, {passive: true});

     

    아예 touch-actionpointer-events와 같은 CSS로 이벤트 핸들러를 제한할 수 있다.

    #area {
      touch-action: pan-x;
    }

     

    아예 다른 방법도 있다.

     

    MDN의 Pointer Events를 읽다보면 스크롤 차단을 피할 수 있게 만들어졌다고 나온다!! [Using Pointer Events, Building a great touchpad experience for the web with Pointer Events]

    Pointer capture allows events for a particular pointer event to be re-targeted to a particular element instead of the normal hit test at a pointer's location. This can be used to ensure that an element continues to receive pointer events even if the pointer device's contact moves off the element (for example by scrolling).

     

    Comparison of Pointer and Touch Event Scroll Initiation GIF

     

    여러모로 포인터 이벤트는 정말 좋으니 잘 써먹도록 하자.

     

    requestAnimationFrame 활용

    입력 핸들러는 request AnimationFrame 콜백 직전에 실행되며, 이때 스타일을 변경하면 강제동기 레이아웃이 발생한다.

    만약 디바운스처리를 할 수 있다면 스크롤시 응답성이 향상되겠죠. [JS 지연처리: 디바운스와 스로틀, 스로틀(throttle)과 디바운스(debounce)]

    function onScroll (evt) {
    
      // Store the scroll value for laterz.
      lastScrollY = window.scrollY;
    
      // Prevent multiple rAF callbacks.
      if (scheduledAnimationFrame)
        return;
    
      scheduledAnimationFrame = true;
      requestAnimationFrame(readAndUpdatePage);
    }
    
    window.addEventListener('scroll', onScroll);

     

     

    - 메뉴 응답성 높이기

    앞서 흑마법이라며 칭송했던 것이 드디어 나왔다!! [아마존의 메가 드롭다운 메뉴 분석]

    더보고 말것도 없다. 일단 보자.

    정말 레이턴시가 없고 미친듯이 빠르지 않은가??

    처음봤을 때 솔직히 감동 받았다. ㅠㅠㅠ

     

    원리는 당연하게도?

    파란 삼각형 내부로 움직이면 유지, 밖이면 탈출하여 다른 메뉴를 보여주는 것.

     

    지금까지 본 UX 트릭중 가장 감동받은 것.

    몇년전 이 글 때문에 UX에 관심을 가지게 되었다.

     

    jQuery-menu-aim(Demo)이 원조고 이를 기반 혹은 비슷한 라이브러리들이 만들어졌다.

     

    빠른 것들을 쓰다가 이제 보편적인 메뉴(라고 쓰고 전투력 측정기라고 읽는 것)들과 비교해보자.

     

    CSS로만 멀티레벨 메뉴를 구현해도 빠르긴 하지만

    범위를 벗어나면 꺼져버리니 느린것만 못한 UX.

     

    유명한 이커머스 업체인 쿠팡에서도 이를 적용하고 있는듯 하다.

    쿠팡

     

    +.

    응답성에 관련된 글을 찾다보니 재귀적인 setTimeout을 이용한 기법이 나오는데 현대 브라우저 기준으론 비효율적인 아이디어라 생각한다. [응답없는 페이지가 되지 않게 하는 법 (feat. setTimeout), 스케쥴링: setTimeout and setInterval]

    let i = 1;
    setTimeout(function run() {
      func(i);
      setTimeout(run, 100);
    }, 100);

     

    오래 걸리는 작업은 워커 쓰레드를 이용해처리하고 메인쓰레드에서 꼭 유지시킬 필요가 있다면 requestIdleCallback을 사용하는게 낫다고 본다.

     

    그래도 +로 적었으니 팁을 적자면

    Timer Resolution이 윈도우(15ms), 리눅스(10ms)정도도되며,  브라우저에서 setTimeout(4ms)와 setInterval(10ms)의 각지연이 더 필요하다. [Windows Timer Resolution: Megawatts Wasted, High Resolution Timers, Timer resolution in browsers]

     

    따라서 약 25(15 + 10)ms보다 짧은 간격을 사용하는 것은 권장하지 않는다.

    그런데 25ms만에 끝날작업은 애초에 응답성 향상을 위해 재귀적 setTimeout을 이용한 기법을 사용할 이유가 없으며, 커다란 작업을 하기에는 타이머가 너무 빈번히 일어난다고 생각한다.

    250ms나 500ms 정도로 넉넉하게 간격을 두고 처리하고, 타이머를 여러개 설정해서 경쟁하지 않도록 하는 것이 좋을 듯.

     

    4. 로드 후

    동일한 페이지, 다른 페이지

     

    4.1 요청줄이기

    다른 페이지로 넘어갈 때 요청량을 줄일 수 있는 여러 기법들이 존재한다. [Offline First]

     

    4.1.1 클라이언트 저장소들 비교와 활용

    개요

    로드 후, 브라우저에 정보를 저장하는 것은 캐시로 사용해 서버에 요청을 줄이거나 낙관적 UI(Optimistic UI)를 구현할 때 커다란 도움이 될 수 있기 때문에 중요하다.

    크로미움의 어플리케이션 탭에 들어가면 스토리지 항목 5개, 캐시 2개가 등장한다.

    이를 Storage, DB, Cookie, Cache로 나누어 간단히 정리해보았다. [클라이언트 측의 저장소 살펴보기, 쿠키 vs 로컬스토리지: 차이점은 무엇일까?, 브라우저 쿠키(Cookie), 세션스토리지(Session Storage), 로컬스토리지(Local Storage), Working with quota on mobile browsers]

     

    Storage

    Web Storage는 클라이언트에 데이터를 저장하기 위해 만들어진 HTML5의 새로운 기능이다.

    Key-Value 방식으로 이루어진다.

     

     

    DB

    웹어플리케이션을 만들도록 도와주는 브라우저 내부에서 사용할 수 있는 DB다. [Beyond HTML5: Database APIs and the Road to IndexedDB, IndexedDB 간단 정리하기]

     

    IndexdDB: Key-Value 방식(JSON), 50MB, 모든 탭, 만료기간 없음

    Web SQL(Deprecated): SQLite Wrapper, 50MB, 모든 탭, 만료기간 없음

     

    Web SQL은 표준화를 시키기 어려워서 Deprecated 되었다.

    This document was on the W3C Recommendation track but specification work has stopped. The specification reached an impasse: all interested implementors have used the same SQL backend (Sqlite), but we need multiple independent implementations to proceed along a standardisation path.

     

    Cookie

    Cookie[HTTP Cookie, Document.cookie, Javascript cookies]에는 저장 유형, 연결 유형에 따라 여러가지로 나뉘기도 하나 여기에서는 Session Cookie와 Persistent Cookie만 다룬다. [HTTP Cookie - Terminlogy]

     

    Key-Value 방식이지만 사용하기는 Storage보다 불편하다.

    setCookie나 getCookie 함수를 만들어 사용하는 것이 좋다고 생각한다. [쿠키(Cookie) 저장 및 삭제 예제보기]

    가장 큰 특징으로는 서버에 전송되어 저장될 수 있다.

     

    Session Cookie: 4KB, 모든 탭, 브라우저 종료 시 삭제

    Persistent Cookie: 4KB, 모든 탭, 만료시기가 되면 삭제

     

    Cache

    둘다 리소스를 캐싱하여 속도를 빠르게 만들고, 서버의 로드비용을 아껴준다. [Browser Cache vs HTML5 Application Cache, Browser Cache Vs HTML5 Application Cache]

    그러나 Application Cache는 Deprecated가 되었기 때문에 서비스워커 + 캐시 API로 마이그레이션을 해야 한다. [Application Cache를 사용하지 않는 사이트]

     

    해결방안

    일단 Deprecated된 Web SQL과 Application Cache는 사용하지 않는 것이 좋다.

    Storage와 Application Cache는 메인 스레드를 차단하는 동기 API이므로, 대규모 데이터를 사용하려면 비동기적이고 웹워커에서 접근 가능한 IndexedDB를 사용하는 것이 좋다. [Progressive Web App용 오프라인 저장소]

    또한 The structured clone algorithm을 사용하여 파일에서 JSON을 저장하거나 검색하는 것보다 낫다.

    단, IndexedDB의 API가 상당히 복잡한편 이라는 것.

     

    Basket.js를 사용하면 localStorage에 저장(캐싱)하는 것이 가능하다.

    단, 요즘은 그냥 서비스워커 + 캐시로 사용하는 것이 좋다고 생각.

    서비스 워커 사용법은 앞서 언급했으니 참고바란다.

     

    4.1.2 쿠키 사용 줄이기

    개요

    HTTP 쿠키들(cookies)은 인증 및 개인화와 같은 다양한 이유로 사용된다. 

    쿠키들에 대한 정보는 서버에 전송되며 교환되므로 성능과 보안에 문제가 생길 수 있다.

    쿠키들의 크기를 가능한 한 작게 유지하는 것은 사용자의 응답시간에 미치는 영향을 최소화하기 위해 매우 중요하다. [When the Cookie Crumbles]

     

    해결방안

    링크의 내용을 정리하면 다음과 같다.

    • 불필요한 쿠키들을 제거
    • 사용자의 응답시간에 미치는 영향을 최소화하기 위해 쿠키들의 크기를 가능한 한 작게 유지
    • 다른 서브도메인들이 영향을 받지 않도록 적절한 도메인 수준(level)에서 쿠키설정을 주의
    • 적절한 만료(Expires) 날짜를 설정
      이른 만료날짜를 설정하거나 설정하지 않으면, 사용자의 응답시간 개선을 위해 쿠키를 즉시 제거
    • 클라이언트에 정보를 저장해야 하는 경우 Storage나 IndexedDB 고려

    3번째 항목을 조금 더 설명하자면 쿠키 free 도메인을 사용하자는 뜻이다.

    정적 이미지와 같은 컨텐츠를 요청하며 쿠키를 함께 보내는 것은 서버에 아무런 영향을 끼치지 않는다.

    쓸데없는 트래픽만 유발하는 행위.

     

    만약 도메인이 www.example.org이라면, static.example.org에 정적 구성요소들을 호스팅 할 수 있다.

    그러나 www.example.org가 아니라 최상위 도메인 example.org에 쿠키들을 설정했다면, static.example.org로의 모든 요청은 쿠키들을 포함한다. 

    이 경우, 완전히 새로운 도메인을 구입할 수 있고, 거기에 정적 구성요소들을 호스트 할 수 있다. 

    또한 이 도메인은 cookie-free 상태로 유지될 수 있다. 

    Yahoo는 yimg.com을 사용, YouTube는 ytimg.com을 사용하고 Amazon은 images-amazon.com 등을 사용한다.

     

    Cookie-free 도메인에서 적정 구성요소들을 호스팅 하는 것의 또 다른 이점은 일부 proxies가 쿠키들과 함께 요청된 구성요소들을 cache하는 것을 거부할 수도 있다는 것이다. 

    같은 맥락으로, 홈페이지에 example.org 또는 www.example.org의 사용을 망설이고 있는 중이라면, 쿠키에 미치는 영향을 고려해야한다.

    www를 생략하는 경우는 *.example.org의 쿠키들을 작성하는 것 밖에 할 수 없지만, 성능을 위해 www 서브도메인을 사용하고 그 서브도메인에 대한 쿠키들을 작성하는 것이 최선의 방법이다.

     

    4.1.3 캐시 제어문  헤더 추가

    개요

    분류: server

    이 규칙에는 두 가지 측면이 있다.

    • 정적 components: 미래 설정 시점까지의 “Never expire” 정책 수행
    • 동적 components: 조건적 요청의 브라우저를 수행하기 위한 적절한 Cache-Control 헤더의 사용

    페이지를 처음 방문한 사용자는 여러 개의 HTTP 요청을해야 할 수도 있지만 캐시 제어문 헤더를 사용하면 해당 구성 요소를 캐시 가능하게 만들 수 있다.

    이는 후속 페이지 로딩 시에서 불필요한 HTTP 요청 수 및 크기를 피하고 로딩속도를 빠르게 만든다. 

    이미지와 함께 가장 많이 사용하지만 스크립트, 스타일 시트를 포함한 모든 구성 요소 에 사용해야한다 .

     

    캐시는 프록시나 CDN등에서 하는 Shared Cache, 브라우저에서 하는 Local Cache로 나눌수 있다.

     

    웹 서버에서 Cache-Control, Expires, Pragma로 제어할 수 있는데 Cache-Control이나 Expires를 사용하는 것을 권장한다. [캐싱 작동 방식]

    - Cache-Control

    • 요청(Request), 응답(Response)
    • 후에 설명할 Expires 헤더의 제한 사항 해결을 위해 HTTP 1.1에 도입됨
    • 따라서 Expires와 충돌 시 우선한다.
    • 헤더 종류
      • no-store: 캐시하지 않고, 항상 다시 다운로드
      • no-cache: 캐시하지만, 재검증을 위한 요청
      • must-revalidate: 만료된 캐시만 재검증
      • public | private: public은 프록시에 저장을 허용하고, private는 브라우저에만 저장을 허용
      • max-age=<second>: 리소스가 유효하다고 판단하는 최대시간을 명시, 0이면 no-cache와 같음

    예시

          Cache-Control:public, max-age=31536000

     

    - 만료(Expires)

    • 응답(Response)
    • HTTP 1.0에 도입
    • <http-date> 값을 사용하며 max-age와 비슷하다.

    예시

    2020년 4월 15일까지 유효한 브라우저를 알리는 미래 시점의 Expires header이다.

          Expires: Wed, 15 Apr 2020 20:00:00 GMT

     

    - Pragma

    • 요청(Request)
    • HTTP 응답에서 명시되지 않았던 헤더라 Cache-Control의 신뢰할 만한 대체제가 아니며, 지원하지 않는 곳들도 많다.
    • no-cache: Cache-Control의 no-cache와 같다.

    예시

          Pragma: no-cache

     

    미래시점에 만료되는 헤더를 사용하면 사용자가 이미 사이트를 방문한 후에만 ​​페이지뷰에 영향을 준다.

    사용자가 처음으로 사이트를 방문하고 브라우저 캐시가 비어있는 경우 HTTP 요청 수에는 영향을 미치지 않는다는 말.

    따라서 이 성능 개선의 영향은 사용자가 캐시된 페이지를 방문하는 빈도에 따라 다르다.

     

    Yahoo!의 측정결과 캐시되어 있는 페이지 조회수는 75-85 %이었다.

    미래시점의 헤더를 사용하면 사용자의 인터넷 연결을 통해 단일 바이트를 보내지 않고도 브라우저에서 캐시하고 후속 페이지뷰에서 재사용되는 구성 요소의 수를 늘릴 수 있다.

    Ajax나 SPA에서도 캐싱을 적극적으로 활용하자.

     

    해결방안

    - 웹서버

    aphache는 .htaccess 파일을 이용해 설정할 수 있다.[Caching Guide, mod_expires, Speed Up Sites with htaccess Caching]

    <ifModule mod_expires.c>
    ExpiresActive On
    ExpiresDefault "access plus 1 month" # years | months | weeks | days | hours | minutes | seconds
    ExpiresByType image/gif "access 3 minutes"
    ExpiresByType image/png "access 3 minutes"
    ExpiresByType image/jpg "access 3 minutes"
    ExpiresByType text/css "access 3 minutes"
    </ifModule>
    
    <ifModule mod_headers.c>
    # YEAR
    <FilesMatch "\.(ico|gif|jpg|jpeg|png|flv|pdf)$">
      Header set Cache-Control "public, max-age=29030400"
    </FilesMatch>
    # WEEK
    <FilesMatch "\.(js|css|swf)$">
      Header set Cache-Control "public, max-age=604800"
    </FilesMatch>
    # 45 MIN
    <FilesMatch "\.(html|htm|txt)$">
      Header set Cache-Control "max-age=2700"
    </FilesMatch>
    </ifModule>

     

    nginx.conf를 다음처럼 설정해볼 수 있다 [A Guide to Caching with NGINX and NGINX Plus, Nginx 캐시 설정 및 캐시 만료일 기간 설정, Nginx Caching]

    # cache.appcache, your document html and data
    location ~* \.(?:manifest|appcache|html?|xml|json)$ {
        expires -1;
        # access_log logs/static.log; # I don't usually include a static log
    }
    
    # Media: images, icons, video, audio, HTC
    location ~* \.(?:jpg|jpeg|png|gif|ico|gz|svg|svgz|ogg|mp4|webm|ogv|htc|cur)$ {
        expires 3M;
        access_log off;
        add_header Cache-Control "public", max-age=31536000;
    }
    
    # Source File
    location ~* \.(?:css|js)$ {
        expires 1M; 
        access_log off;
        add_header Cache-Control "public";
    }
    
    # Favicon
    location = /favicon.ico {
        expires max;
        access_log off;
        log_not_found off;
    }
    
    # Feed
    location ~* \.(?:rss|atom)$ {
        expires 1h;
        add_header Cache-Control "public";
    }

    sane-caching.nginx.conf(gist)에도 잘 나와있다.

     

     

    php의 경우 다음과 같이 해볼 수 있는 모양이다. [header]

    <?php
      header("Cache-Control: public, max-age=31536000");
      //or
      header("Expires: Wed, 15 Apr 2020 20:00:00 GMT");
    ?>

     

    expressjs를 사용할 경우 web.dev의 문서를 참고할 수 있다.

     

     

    - meta 태그 사용

    과거 브라우저에서 사용하고, 현재는 잘 사용하지 않는 방식.

    다음과 같이 사용할 수 있다. [A Dictionary of HTML META Tags]

    <meta http-equiv="Cache-control" content="public, max-age=31536000">
    <meta http-equiv="expires" content="Wed, 15 Apr 2020 20:00:00 GMT">

     

    - 기한

    구글이 제공하는 HTTP Caching 문서에서는 다음과 같이 되어 있다.

    • HTML: no-cache
    • CSS: public, max-age=31536000(1년)
    • JS: private, max-age=315360000
    • Image: max-age: 86400

    JS파일에 개인정보가 없다면 public으로 해도 상관은 없다.

     

    즉, 다음을 고려하여 설정해야 한다.

    • 각 리소스별 최적의 캐시 수명
    • 프록시 캐시 허용 여부

     

    - 기타 주의사항

    미래에 만료되는 헤더를 사용하는 경우 구성 요소가 변경 될 때마다 구성 요소의 파일 이름을 변경해야 한다.

     

    이 단계를 빌드 프로세스의 일부로 만들 수 있다.

    예를들어 EXAMPLE_2.0.6.js처럼 버전 번호, EXAMPLE_20200402.js처럼 날짜등을 파일 이름에 포함할 수 있다.

     

    webpack의 경우 Caching 문서를 확인해보면 자동으로 생성해준다.[Hash vs chunkhash vs ContentHash]

     

    4.1.4 유효성 검사

    개요

    분류: server

    캐시가 유효하지 않으면(또는 만료되었으면) 다시 서버에 요청을 해야하는 것은 분명하다.

    그런데 매번 새로운 파일을 다운받는 것은 비효율적이므로 서버의 파일이 동일한 것인지 유효성 검사를 하면 다운로드를 줄일 수 있다.

    Cache-Control: must-revalidate이 명시되어 있거나 만료시간이 가까워도 검증은 필요하다.

    유효성 검사 결과 수정되지 않았다면 요청한 리소스 본문대신 304 Not Modified를 보내며, 수정되었다면 200 OK와 함께 새로운 리소스 본문과 새롭게 대체할 유효성 값을 보낸다.

     

    - ETag

    ETag (Entity 태그)는 특정 버전의 구성 요소를 고유하게 식별하는 문자열으로 일종의 해시값이라 생각하면 쉽다.

    "Entity"는 이미지, 스크립트, 스타일 시트 등 "Component(구성 요소)"라는 또 다른 단어.

     

    ETag는 후에 설명할 Last-Modified(마지막 수정 일자)보다 유연한 엔터티를 검증하는 메커니즘을 제공한다. 

    유일한 형식 제한은 문자열을 이용하는 것이다. 

     

    Etag는 강한 검증, 약한 검증이 존재한다. [HTTP 조건부 요청]

    강한 검증은 바이트 대 바이트로 동일한지 보장하는 것으로 MD5같은 해시를 이용한다.

    반면 약한 검증은 유사한 경우 버전이 동일하다고 간주하는 것으로 캐시 성능 최적화에 유용하다.

    약한 검증으로 쓸만한 것은 날짜 혹은 광고만 다른 페이지.

    약한 검증을 하고 싶으면 W/를 앞에 붙이면 된다.

     

          ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
          ETag: W/"0815"

     

    응답 받았을 때 Etag 값은 If-None-Match 헤더에 포함시켜 전송 후 유효성을 판단한다.

     

    - Last-Modified

    Last-Modified는 말 그대로 마지막으로 수정된 날짜를 말하며 Expires의 형식과 같다.

    엄밀하지 않기 때문에 약한 검증용으로 사용할 수 있다.(강한 검증은 불가)

     

    응답 받았을 때 Last-Modified 값은 If-Modified-Since 헤더에 포함시켜 전송 후 유효성을 판단한다.

    Last-Modified와 Etags의 관계는 Expires와 Cache-Control과 비슷해서 동시에 존재하면 Etags를 우선한다.

     

    - 정리

    자, 그러면 처음부터 유효성을 검증하는 과정을 생각해봅시다.

    먼저 Etag나 Last-Modified와 함께 온다.

          HTTP/1.1 200 OK
          Last-Modified: Tue, 12 Dec 2006 03:03:59 GMT
          ETag: "10c24bc-4ab-457e1c1f"
          Content-Length: 12195

     

    나중에 브라우저가 구성 요소의 유효성을 검사해야하는 경우  If-None-Match나 If-Modified-Since 헤더를 사용하여 ETag나 Last-Modified 값을 원래 서버로 다시 전달한다. 

    일치하면 304 예제 코드가 리턴되어 이 예제의 응답을 12195 바이트만큼 줄인다.

          GET /i/yahoo.gif HTTP/1.1
          Host: us.yimg.com
          If-Modified-Since: Tue, 12 Dec 2006 03:03:59 GMT
          If-None-Match: "10c24bc-4ab-457e1c1f"
          HTTP/1.1 304 Not Modified

     

    요청과 응답의 관점으로 보자면 [더 빠른 웹을 위하여 - 웹 캐쉬(WEB CACHE)]

     

    요청

    • 유효성(Validation): If-Modified-Since(1.0), If-None-Match
    • 신선도(Freshness): Pragma(1.0), Cache-Control(1.1)

    응답

    • 유효성(Validation): Last-Modified(1.0), Entity Tag(1.1)
    • 신선도(Freshness): Expires(1.0), Cache-Control(1.1)

    요청과 관련된 추가적인 내용은 강/약한 검증에서 소개했던 HTTP conditional requests에 잘 나와있다.

     

    해결방법

    아파치는 FileETag를, Nginx는 etag 지시어를 이용해 조절 가능하다.

    단, 서버를 여러대 사용할 경우, etag 값은 달라질 수도 있는데 이 경우 Etag를 제거하거나 inode 값을 빼야 한다.

     

    그렇다면 전체적인 캐싱/유효성 검증은 어떠한 방식으로 결정해야 하는가?

    구글에서는 다음 그림을 소개한다.

     

     

    4.1.5 AJAX/Websocket이나 SPA사용

    개요

    분류: Content

    AJAX는 필요한 내용만 웹서버에 요청해 처리할 수 있다. [Ajax]

     

     

    따라서 AJAX를 사용하면 동일한 도메인 내의 페이지를 이동할 때 불필요한 부분을 다운받지 않아 성능에 유리하다.

     

    가 라이브러리로 유명하다.

     

     

    Websocket

    이때 Websocket을 사용한다면 데이터 통신을 더 줄일 수 있다. [HTML5 WebSocket(웹 소켓) 기본 예제 및 설명, Moving Data over the Web: AJAX vs. WebSockets vs. Webhooks]

    말그대로 소켓의 역할을 해서 실시간 업데이트에 유리하다.

    위 그림에 보이다시피 매번 HTTP 헤더가 필요한 AJAX와 달리 한번만 설정하면 바로바로 통신할 수 있다.

     

    웹소켓의 라이브러리는

    가 유명하다.

     

    그런데 항상 빠른 것은 아니다. [Speed dilemma of Node.js: Which do AJAX and Socket.IO choose?, HTTP vs Websockets: A performance comparison]

    웹소켓은 연결을 설정하는데 약 190ms정도의 시간이 필요하기 때문.

    즉,

    • 적은 수의 데이터를 가끔씩 업데이트: AJAX
    • 많은 수의 데이터를 빈번히 업데이트: 웹소켓

    이 좋은 방안이라 할 수 있다.

     

    해결방안

    AJAX의 문제와 해결

    AJAX를 사용시 가장 큰 문제는 주소가 업데이트되지 않아 뒤로가기가 되지 않는다는 점이다. [Ajax를 사용할 때 웹브라우저 "뒤로 가기"의 구현, page-back-example]

    일반 링크 vs AJAX

    이때 해결할 수 있는 방법은 HTML5 HistorypushState()를 사용하는 것이다.

     

    SPA와 렌더링

    이러한 AJAX의 장점을 이용해 페이지를 빠르고 부드럽게 렌더링하는 패러다임이 등장했으니 React, Vue, Angular등으로 유명한 SPA다. [Single Page Application vs Multi Page Application, SPA와 SPA 라우팅 원리, Single Page Application & Routing, 서버사이드 랜더링과 SPA 라우팅의 동작원리]

     

    보다시피 AJAX의 통신방법을 잘 써먹고 있다.

     

    그러나 웹페이지 렌더링을 자바스크립트를 이용해 처리해 초기속도가 느리다는 점, AJAX의 특징등 때문에 다양한 렌더링 방식이 등장하게 되었다. [웹에서의 렌더링, 웹렌더링]

    가급적 링크의 내용을 참고바람. 절대 시간이 아까운 내용은 없다.

    순서대로 Server Rendering, Static Rendering, Client-Side Rendering, SSR+hydration

    • Server Rendering: 요청한 페이지의 전체 HTML을 서버에서 생성해 로드
    • Static Rendering: 빌드시 HTML을 생성하여 배포
    • Client-Side Rendering: 브라우저에서 자바스크립트로 페이지를 렌더링
    • SSR + Rehydration: 캐시성이 높은 컨텐츠에만 SSR을 적용

    정리된 이미지.

     

    물론 가장 좋은 방법은 프로그래시브 렌더링이 되도록 만드는 것.

     

    렌더링 개선법

    CSR의 효율을 최대한 높히기 위해 PRPL(Push Render Pre-Cache Lazy-load) 패턴이 생겼고(물론 우리가 위에서 다루었던 최적화 방법들),

     

    서비스 워커를 사용하면 초기화와 자바스크립트를 사용하지 않은 Navigation에서 스트리밍을 할 수 있다.

     

    MPA에서 SPA처럼

    MPA도 AJAX를 사용하면 SPA처럼 렌더링할 수 있지 않을까 싶어 찾아보게 되었다. 

    그러다 굉장히 흥미로운 구현체들을 보게되었는데 전체 페이지로드를 발생시키지 않고, AJAX를 이용해 페이지의 바뀐부분만 업데이트시키는 방법이었다. [pjax를 이용한 극적인 웹페이지 속도 향상, How We Migrated To Turbolinks Without Breaking Javascript]

     

    쉽게 설명하면

    • head: 병합
    • body: 교체

    와 같은 원리로 사용된다.

     

    대표적인 라이브러리로는

    가 있으며 성능향상이 충분하다.

     

    다음은 PJAX 아이디어를 기반한 라이브러리들.

    turbolinks

     

    얼마나 효과가 있을지 모르겠으나 기존 SPA와 함께 사용할 수도 있는듯 하다.

     

    4.1.6 AJAX 요청에 GET 사용

    개요

    분류: server
    야후! 메일팀은 XMLHttpRequest의 POST를 사용할 때 브라우저에서 헤더를 먼저 보낸 다음 데이터를 보내는 2 단계 프로세스로 구현 한다는 것을 알게되었다 .

    따라서 쿠키가 많지 않은 경우 하나의 TCP 패킷 만 보내면 POST보다 GET을 사용하는 것이 가장 좋다.

     

    IE 기준 최대 URL 길이는 2K이므로 2K 이상의 데이터를 보내면 GET을 사용하지 못할 수 있다.

    다음은 일반적으로 말하는 GET과 POST의 장단점. [GET vs.​ POST]

     

    - GET

    장점

    • 고정적인 주소 및 링크를 사용하기 때문에 캐싱 가능
    • Content-Type이 필요 없으므로(body가 없음) 가벼움, 야후 메일팀이 말한것

     

    단점

    • 길이 제한
    • 주소창에 쿼리 스트링이 보여짐

     

    - POST

    장점

    • 객체를 보낼 수 있음

     

    단점

    • 서버로 보내기 전에 인코딩, 전송 후 디코딩 작업
    • 캐싱이 힘들고, 데이터를 2단계에 걸쳐 보냄

     

    - 기타

    흥미로운 부작용은 포스팅된 데이터가 없는 POST는 GET처럼 동작한다는 것이다.

    HTTP 사양을 기반으로 살펴보면 GET은 fetch(요청)할 때 사용, POST는 수행할 때 사용한다.

     

    CRUD(Create, Read, Update, Delete) 중 Read할 때만 fetch를 하는 개념이므로 GET을 사용하는 것이 맞다는 것이다.

    그런데 개발자들이 구분하지 않고 사용하여 미리 링크를 클릭해(GET) 성능 향상을 하려 시도했던 구글 Accelerator(전버전)가 실패한 적이 있다나.

     

    4.2 기타

    4.2.1 로드 전 구성 요소 

    개요

    분류: content

    프리로드는 포스트로드와 반대되는 것처럼 보이지만 실제로는 다른 목표가 있다. [The Case for Auto-Preloading: The Anatomy of Battle-Tested WPO Treatment, How To Make Users Think Your App Loa보다 ds Faster, Faster Loading Times with Prefetch, Preload and Prerender, Best-ish Practices for Dynamically Prefetching & Prerendering Pages with JavaScript]

    컴포넌트를 사전로드하면 브라우저가 유휴 상태 인 시간을 활용하고 나중에 필요한 컴포넌트, 이미지, 스타일 및 스크립트를 요청할 수 있다. 

    이렇게 하면 사용자가 다음 페이지를 방문 할 때 이미 캐시에 대부분의 구성 요소가있을 수 있으며, 페이지가 사용자에게 훨씬 빠르게 로드된다.

     

    반대로 업로드도 가능하다.

     

    해결방법

    - 유형

    몇 가지 유형의 사전로드가 있다.

     

    무조건

    onload가 발생하자마자 추가 구성 요소를 가져온다.
    스프라이트 이미지가 onload후 요청되는 방법에 대한 예는 google.com이 있다.

    다운받은 스프라이트 이미지는 google.com 홈페이지에는 필요하지 않지만 연속 검색 결과 페이지에는 필요하다.

     

    조건부

    사용자 조치를 기반으로 사용자가 다음으로 향하는 위치를 정교하게 추측하고 그에 따라 사전로드한다.

    search.yahoo.com의 검색창에 입력을 시작 후 몇 가지 추가 구성 요소를 요청하는 방법을 볼 수 있다.

     

    업로드도 가능하다는 예를 보이기 위해 설명하자면,

    인스타그램은 사용자가 공유할 사진을 고르면 미리 업로드를 백그라운드로 진행하기도 한다.

    인스타그램 공유하기

     

     

    예상

    redesign을 시작하기 전에 미리로드한다는 아이디어.

    "새 사이트는 멋지지만 이전보다 느리다."라는 말은 redesign 후 흔히 들을 수 있다.

     

    문제는 사용자가 전체 캐시를 사용하여 이전 사이트를 방문하고 있었지만, 새 사이트는 항상 빈 캐시 환경이라는 것이다.

    redesign을 시작하기 전에 일부 구성 요소를 사전로드하여 이 부작용을 완화 할 수 있다.

    기존 사이트는 브라우저가 유휴 상태인 시간을 사용하고 새 사이트에서 사용될 이미지 및 스크립트를 요청할 수 있다.

     

    인스타그램의 경우 컨텐츠를 순서대로가 아니라 중요도로 예측하여 다운받는다.

    SNS이므로 많은 사용자가 좋아하는 컨텐츠에 들어갈 확률이 관심을 적게 받는 컨텐츠보다 높기 때문.

     

    사실 무조건, 조건부, 예상이든 모두 사용자가 사용할 것이라 '예측'하기 때문에 로드한다.

     

    - 기술

    리소스 힌트를 사용하면 미리 캐싱을 할 수 있다. [Prefetching, preloading, prebrowsing, Prebrowsing, Best-ish Practices for Dynamically Prefetching & Prerendering Pages with JavaScript, Resource Hint, Web Developer's Guide to Prerendering in Chrome]

     

    Prerender(실험적)

    이 중 아직 언급하지 않았던 prerender를 다루어보려 한다.

    prefetch와 같이 다음탐색에 요구될 수 있는 자원에 대해 더 빠른 응답에 대한 준비, 실행을 하도록 만든다.

    차이점이라면 브라우저가 백그라운드에서 URL을 가져오고 렌더링하여 즉각적으로 보여줄 수 있다. [Document.visibilityState]

    문제는 페이지를 통째로 다운받기 때문에 리소스 사용량이 많다는 것.

    따라서 거의 무조건 넘어갈 것이라 예상할 때만 이 힌트를 사용해야 한다.

     

    정리

    • 예측되는 도메인: dns-prefetch, preconnect
    • 예측되는 리소스: prefetch
    • 예측되는 페이지: prerender

     

    - 또 다른 활용방법

    위에서 나온 예시들 말고 한가지 흥미로운 예측방법을 소개하려한다. [Prerender on hover?]

    화면에 링크가 보이거나 마우스를 링크에 올리면 들어갈 확률이 높기 때문에 미리로드를 하자는 것이다.

     

    이를 도와주는 라이브러리가 존재한다. [Quicklink vs Instant.page vs Flying Pages – Why I built Flying Pages]

     

    Hover 기반에서 문제는 호버 후 서버응답시간(약 300ms)가 누르는 시간보다 짧을 수 있다는 것이다.

    이럴 때는

    처럼 마우스 움직임을 예측하도록 도와주는 라이브러리를 쓸 수 있다.

     

    한편, 지금까지와 반대로 hoverintent(jQuery plugin)처럼 너무 민감하지 않게 만드는 방법이 필요할 수도 있다. [Faking Hover Intent with JavaScript]

    사전에 로드하는 것은 리소스를 소모하는 작업이기 때문.

     

    빠른 페이지 렌더링과 관련된 기술과 융합해 사용할 수도 있다.

     

    참고

    이 글을 쓸때 참고한 목록들입니다.

    구글

     

    야후

     

    TOAST UI

     

     

    MDN

     

     

    기타

     

    Other Platform

     

    댓글 0

Designed by black7375.