웹폰트 최적화 기법에 관한 몇가지 이야기

요근래 폰트 최적화에 관해 몇가지 고민할 일들이 생겼다.
구글과 여러 사이트들을 참조하며 정리한 내용들을 공유하고자 한다.

기존의 폰트 로딩 방식

body {
    font-family: "Nanum Gothic",Meiryo,"Noto Sans JP",sans-serif,Lucida Sans Unicode;
}

폰트는 font-family는 통해 적용한다.
적용 시점은 dom과 cssom을 만들고 페이지에서 지정된 텍스트를 렌더링 하는데 필요한 글꼴이 있으면 그 순간 적용된다.

하지만 컴퓨터에 글꼴이 설치되어 있지 않으면 서체를 보여주지 못한다.

그래서 나온게 @font-face이다. font-face는 해당 폰트가 시스템에 없으면 직접 경로를 통해 다운로드 하게 설정할 수 있다. 이를 웹폰트라한다.

@font-face {
  font-family: 'webFont';
  src: url('webfont.woff2') format('woff2'), /* 모던 브라우저. 압축률이 가장 높음(30%) */
       url('webfont.woff') format('woff'), /* 대부분 브라우저. 압축률이 좋음*/
       url('webfont.ttf')  format('truetype'), /* Android, iOS, 압축 x */,
       url('webfont.eot'); /* IE9 호완성 모드, 압축 x */
}
body {
    font-family: webFont, "Nanum Gothic", Meiryo,......
}

하지만 웹폰트에는 대표적인 두가지 문제점이 있다.

FOIT

Flash of Invisible Text의 약자

  • 웹폰트가 로드될때까지 텍스트를 랜더링 하지 않다가 로딩이 된 이후에 텍스트를 보여주는 동작이다.
  • 폰트가 로딩이 되지 않으면 웹페이지의 블락을 가져오기 때문에 가급적 피해야한다.
  • 모던브라우저의 경우는 기다리는 제한시간이 있다.

FOUT

Flash of Unstyled Text의 약자

  • 웹폰트가 로드될때까지 시스템의 기본 폰트를 보여주고 로드가 되면 reflow해서 글꼴을 대체하는 방식
  • FOIT보다 사용하는 측면에서는 더 괜찮을 수 있다. 사용자는 폰트보다 내용에 집중하기 때문이다
  • 흔히 말하는 브라우저의 깜빡임이다.
  • 글꼴의 자간, 높이에 따라 레이아웃이 변경될 수 있다. 이럴경우 레이아웃이 틀어져서 이상해보인다.
    • 그럴땐 font-size-adjust속성이나 letter-spacing, line-height 등을 조정하여 최대한 적게 변해보이도록 바꿔야한다.

둘 중 뭐가 나쁜지는 생각의 문제이다. 하지만 현재는 cpu와 네트워크 속도가 어느정도 빠르고 초기 로딩의 경험이 가장 중요하다고 생각한다. 페이지는 로딩할 때 빈 텍스트라면 그게 가장 큰 문제라고 생각한다.

각 브라우저는 어떻게 동작할까

크롬과 파이어폭스의 (+ 사파리) 경우는 FOIT이다.
일단 3초동안 웹폰트가 로드되기를 기다린다. 그 동안은 빈 화면이 나타난다. 만약 3초동안 오지 않으면 시스템 기본폰트를 보여준다.

크롬의 경우 아래와 같은 메시지가 뜬다.

IE와 EDGE는 FOUT이다. 일단 시스템폰트를 보여주고 웹폰트가 오면 대체한다.

구형 사파리의 경우는 시간제한 없이 FOIT로 웹폰트를 기다렸다. 60초가 넘으면 응답자체가 취소 되기 때문에 콘텐츠 자체를 볼 수 없는 원인이 되기도 하였다. 다행이도 최근에는 timeout을 적용하였다.

압축과 캐싱.

  • https://en.wikipedia.org/wiki/Zopfli 압출을 하면 5%의 파일크기 절감 효과.
  • 폰트자체는 바뀔일이 거의 없으므로 max-age를 길게 잡고 캐싱해도 됨.

@font-face의 여러 속성들 활용

호환성에 대해 알아야 할 부분

  • IE8이하나 구 모바일 브라우저를 지원하지 않는다면 svg나 eot를 지원해줄 필요는 없다.

font의 지원범위

  • format으로 폰트 타입을 명시해주지 않으면 브라우저는 일단 정의된 순서대로 다운을 받고 해당 폰트를 쓸 수 있는지 검사한다. 그러므로 반드시 써줘야 한다.

font-display

  • 브라우저가 폰트를 어떻게 보여 줄 것인지를 설정할 수 있다.
  • 아래와 같은 옵션이 있다.
    • auto - 브라우저의 기본동작에 맡기는 방식이다.
    • block - FOIT 즉, 타임아웃까지 텍스트를 보여주지 않음
    • swap - 응답이 올때까지 무한정 기다리고 그 전 까진 바로 기본폰트를 보여준다. 꼭 적용해야만하는 중요폰트일 경우에 쓸 수 있다.
    • fallback - 100ms내외의 시간동안만 block을 하고 기본폰트를 보여준다. 응답이 오면 해당 폰트로 swap하지만 짧은 시간만 기다린다.
    • optional - 100ms내외의 시간동안만 block을 하고 기본폰트를 보여준다. 그 후에는 대체를 하지 않는다.
  • fallback정도가 가장 좋은 옵션이 될 것 같다.
@font-face {
    font-family: 'Roboto';
    font-style: normal;
    font-weight: 300;
    font-display: fallback;
    src: url(#{$font-folder}/Roboto-Light.woff2) format('woff2'),
    url(#{$font-folder}/Roboto-Light.woff) format('woff'),
    url(#{$font-folder}/Roboto-Light.ttf) format('truetype');
}

local()의 활용

font-face를 쓴다면 font가 시스템에 있는지의 여부와 상관없이 네트워크로 부터 폰트를 다운을 받는다. 그래서 local을 먼저 앞에 선언해 시스템에 있는 것을 쓰도록 해야한다.

@font-face {
    font-family: 'Nanum Gothic';
    font-style: normal;
    font-weight: 400;
    src: local('Nanum Gothic'),
    url(#{$font-folder}/NanumGothic-Regular.woff2) format('woff2'),
    ...
}

두레이에 적용해보면 네트워크에서 받지도 않고 시스템으로 적용된 것을 볼 수있다.


unicode-range

font face에는 해당 폰트가 지원할 범위를 설정 할 수 있다. 그래서 해당 범위의 문자가 있을 때에만 폰트를 다운받도록 변경이 가능하다.
현재 모든 모던 브라우저 지원이다.

@font-face {
  font-family: 'Ampersand';
  src: local('Times New Roman');
  unicode-range: U+26;
}

한글일 경우 아래정도이다.
한글 : U+0020-U+007E,U+1100-U+11F9,U+3000-U+303F,U+3131-U+318E,U+327F-U+327F,U+AC00-U+D7A3,U+FF01-U+FF60

font-variation-settings

font-variation-settings를 이용하면 가변으로 폰트를 설정할 수 있다.

src: url('source-sans-variable.woff2') format('woff2-variations')
  • true-type으로 받게 되면 unicode범위와 상관없이 글꼴을 표현 할 수 있다.
  • 굵기가 100단위가 아니라 세부단위로 지정이 가능하다.
  • 브라우저 지원이 미약하고 true-type의 용량이 크다. 하지만 woff2로 몇몇 유니코드 지원만으로 variations을 만들면 쓸 수 있다.
  • 여러 글로벌 서비스를 지원하는 경우 유리하다.

참고: https://medium.com/clear-left-thinking/how-to-use-variable-fonts-in-the-real-world-e6d73065a604

preload

link태그에 preload라는 옵션이 있다. 이 옵션을 가진 리소스는 다른 어떤 리소스보다 빨리 받아온다. font뿐만 아니라 이미지, 비디오, 스크립트등에도 가능하다.
그래서 이것을 쓰면 로딩되기전에 미리 폰트를 받아 올 수 있다.

<link rel="preload" href="fonts/cicle_fina-webfont.woff2" as="font" type="font/woff2" crossorigin="anonymous">
  • font-face block이나 webfontloader와 같이 쓰면 효과가 좋다.
  • 많이 쓰면 쓸수록 초기 렌더링시간이 지연되므로 중요한 하나정도의 폰트만 로딩하는게 좋다. 이왕이면 가벼운 woff나 woff2가 좋다.

  • 다만 아쉽게도 브라우저 지원이 좋지 않다. 곧 지원될 것이라 생각한다.

FOUT with class

FOIT를 피하기 위한 간단한 방법은
폰트를 스크립트에서 로딩하고 완료되면 class를 동적으로 넣어서 FOUT처럼 동작하게 하는 방법이 있다. webfont가 먼저 필요하지 않으니 브라우저는 기본폰트를 보여주고, 로딩이 완료되면 폰트가 적용되기 때문에 FOIT를 피할 수 있다.
돔을 그리기 전에 미리 폰트를 로딩을 한다면 FOUT 또한 피할 수 있다.

Font Face라는 api가 이미 구현이 되어있다.
)

document.fonts.load('1em open_sansregular')
    .then(function() {
        var docEl = document.documentElement;
        docEl.className += ' open-sans-loaded';
    });

다만 현재는(2018.1) 크롬과 파이어 폭스에만 제대로 구현이 되어있다.
그래서 polyfill이나 webfont-loader같은 라이브러리를 써야한다.

webfont-loader

webfont-loader는 구글에서 만든 동적 폰트 loader라이브러리다. head태그 안에 넣으면된다.

폰트가 로딩이 되면 사진과 같이 class가 추가가 된다.

비동기로 로드 할 경우 로딩 시점이 다를 수 있으므로 FOUT가 발생할 수 있다.

<script>
   WebFontConfig = {
      typekit: { id: 'xxxxxx' }
   };

   (function(d) {
      var wf = d.createElement('script'), s = d.scripts[0];
      wf.src = 'https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js';
      wf.async = true;
      s.parentNode.insertBefore(wf, s);
   })(document);
</script>

동기적으로 로딩하면 FOUT도 피할 수 있다. 하지만 그만큼 랜더링이 블락된다.

  global.WebFont.load({
            custom: {
                families: ['dcon', 'Nanum Gothic', 'Noto Sans JP'],
                urls: ['/assets/styles/lazy/iconfont.css', '/assets/styles/lazy/cal-iconfont.css', '/assets/styles/lazy/fonts.css']
            }
        });

fontfaceobserver

font face observer는 웹폰트로더랑 동작은 비슷하지만 스크롤이벤트가 발생할때마다 폰트로드를 확인하기 때문에 더 가볍다. 소스또한 경량이다.

var font = new FontFaceObserver('My Family');

font.load().then(function () {
  document.documentElement.className += " fonts-loaded";
});

.fonts-loaded {
  body {
    font-family: My Family, sans-serif;
  }
}

FOFT

Flash of Faux Text의 약자.

FOUT를 조금 더 개선한 최적화 기법이다. 폰트의 subset 폰트를 먼저 뽑아서 적용시킨다. 일만적으로 normal폰트를 뽑는다. 더 빠르게하려면 자주 쓰는 unicode 범위의 문자만 뽑아 적용시킬수도 있다. 그 후 나머지 볼드, 이태릭의 폰트들을 불러오는 방식이다.

subset폰트는 파일이 가볍고 빠르기 때문에 일반적인 FOUT보다 더 빠르게 화면을 볼 수 있다. 그 후 나머지 무겁지만 중요하지 않은 폰트를 다운받아서 적용하는 것이다.

어떻게 쓰는지 https://www.zachleat.com/web-fonts의 데모를 살펴보면

var fontASubset = new FontFaceObserver('LatoSubset');

Promise.all([fontASubset.load()]).then(function () {
    document.documentElement.className += " fonts-loaded-1";

    var fontA = new FontFaceObserver('Lato');
    var fontB = new FontFaceObserver('LatoBold', {
            weight: 700
        });
    var fontC = new FontFaceObserver('LatoItalic', {
            style: 'italic'
        });
    var fontD = new FontFaceObserver('LatoBoldItalic', {
            weight: 700,
            style: 'italic'
        });

    Promise.all([fontA.load(), fontB.load(), fontC.load(), fontD.load()]).then(function () {
        document.documentElement.className += " fonts-loaded-2";

        // Optimization for Repeat Views

        sessionStorage.fontsLoadedCriticalFoftPolyfill = true;
    });
});

FontFaceObserver옵져버를 사용하여 먼저 subset폰트를 로드하고 로드가 된 이후에 나머지를 로드한다. class를 2개로 분리하여 bold를 쓰는 것들은 두번째 로드가 된 이후에 적용시키면 된다.

.fonts-loaded-2 em {
    font-family: LatoItalic, sans-serif;
    font-style: italic;
}

font-face에 font-synthesis라는 속성을 사용하여 처리 할 수도 있다. 다만 브라우저 호환이 아직 좋지 않아서 지금은 위와 같이 쓰는게 좋다.

.syn {
  font-synthesis: style weight;
}
.no-syn {
  font-synthesis: none;
}

base64 font

  • font를 data-uri로 가지고 css에 포함시키는 방법이다.
  • 아래 그림을 보면 랜더링을 하는 시점은 first-paint가 되는 시점이다. 일반적인 폰트 로딩 시점은 cssom은 이미 만들어 져있는 상태이고 first-paint를 그리는 시점이 될 것이다.
  • 그래서 data-uri(base64)로 css에 폰트를 포함시키면 css 로드 시점에 폰트를 읽는다. 즉 FOIT와 FOUT가 없다.
  • css는 폰트를 하나씩 읽게되고 css파싱은 길어지게 된다. 그만큼 첫번째 페인팅시점이 느려지므로 작은용량의 폰트에만 써야 한다.
  • css를 파싱하느라 paint가 느려지지 않게 스크립트에서 css를 비동기적으로 로드 할 수도 있다.
    • css load도 작은폰트가 들어있는 파일이 먼저 되게 FOFT를 같이 적용하면 좀 더 빠르다.

sessionstorage 트릭

sessionstorage에 폰트를 받았는지 여부를 저장해놓고 네트워크 비용을 줄 일 수 있다.
단 메모리에 캐싱이 가능한 상태여야 한다. 위의 fout, foft와 base64 비동기 로딩 방식과 같이 쓸 수 있다.
시작 할 때 메모리에 있는지 검사함으로서 네트워크 요청을 줄일 수 있다.

if(sessionStorage.fonts) {
    document.body.className+=' fontsloaded
}
....중략

//google webfontlaoder
 active: function() {
    sessionStorage.fonts = true;
  }

or

.load(() => {
    sessionStorage.fonts = true; 
    document.body.className+=' fontsloaded;
});

localstorage에 저장하기

  • localstroage에 폰트를 base64로 저장해놓고 로드하여 fout를 방지하는 방법이다.
  • 사실 찬반이 조금 갈리는 방법이다.
  • 구글에서는 권장하고 있지 않다. 폰트만의 고유 성능 문제가 있기 때문이라고..
  • FOIT와 FOUT를 없앨 수 있다.
  • localstorage는 캐싱보다 느리다. 그래서 캐싱을 쓰는것이 더 좋다
  • 캐싱이 자주 풀리는 모바일 브라우저의 경우는 유용할 수 있다.

또다른 신기한 방법

css를 media="none" 을 두고 로딩하면 랜더링을 블락하지 않는다고 한다. 이게 잘 된다면 그동안 block을 피하기위해 했던 방법들중에 가장 편하고 좋은게 아닐까 싶다.
참고

<link rel="stylesheet" type="text/css" href="fonts.css" media="none" onload="this.media='all';">
<link rel="stylesheet" type="text/css" href="style.css" media="none" onload="this.media='all';">

참고

https://dev.opera.com/articles/better-font-face/
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization?hl=en#defining-font-family-with-font-face
https://www.zachleat.com/web/comprehensive-webfonts/
https://davidwalsh.name/font-loading

Comments

comments powered by Disqus
comments powered by Disqus