극한의 프론트엔드 성능 최적화 3편 (외부 스크립트 최적화 feat. partytown🎉)

Sming·2023년 5월 18일
30
post-thumbnail

외부 스크립트 최적화하기

이번에 얘기해볼 주제는 외부 라이브러리를 최적화하는 방법입니다.

작은 프로젝트에서는 이러한 외부스크립트를 사용할일이 많이 존재하지않지만 어느정도 커지게 된다면 ga, gtm등 분석하는 도구를 외부 스크립트를 이용할일이 생기며 회사의 레거시때문에 어쩔수없이 외부 스크립트를 이용할때가 있을겁니다.

물론 오늘 얘기할 내용은 외부 스크립트가 아니더라도 비용이 큰 스크립트라면 이 방법으로 최적화 시킬수있습니다.

메인스레드가 블로킹 되지 않게 하기 - 🎉partytown🎉

요즘 가장 빠른 자바스크립트 프레임워크로 뜨고있는 qwik을 만든 builder.io에서 만든 라이브러리입니다.

이 라이브러리의 핵심은 바로 web worker입니다. web worker 는 비용이 오래 걸리는 코드가 메인 스레드를 블로킹하는것을 막기위해서 비용이 오래걸리는 코드만 다른 스레드에서 처리할수 있는 하나의 서비스 워커입니다.

Web Worker API


예를 들어 프로젝트에 for문을 1억번 돌리는것이 있을경우 그 for문이 끝날때까지 그 다음 코드로 넘어가지 않습니다.

이것을 해결하기위해 나온것이 web worker 입니다. 그렇다면 web worker api 자체로 스크립트를 다른 스레드에서 작업하게하면 되는게 아니냐라고 생각할수있지만 web worker에서 할수없는 기능들을 partytown 에서 가능하게 하기때문에 이용합니다.

기존 web worker에서는 window객체, dom에 접근을 할수없습니다.

하지만 요즘 외부라이브러리들은 window객체를 통하여 상태를 저장시키거나 dom에 직접 접근을 하는 경우가 있습니다. partytown은 web worker, proxy, custom event등의 문법의 조합으로 web worker환경에서도 window, dom에 접근이 가능하도록 하였습니다.

밑에는 간단하게 partytown의 동작에 대한 공식 사진입니다.

partytown의 동작 과정

partytown의 동작원리

webworker, proxy 그리고 event를 통하여 web worker의 한계를 극복한 partytown의 구현체입니다.

그래서 어떻게 사용..?

이번에 이용한 프로젝트가 nextjs이기 때문에 nextjs 기준으로 설명드리겠습니다. 그 외의것들도 partytown의 공식문서 에 잘나와있으니 보면서 하면 큰 문제가 없을겁니다.

// layout.tsx

    <Head>
      <Partytown forward={['dataLayer.push']} />
      <script type="text/partytown" src="https://www.googletagmanager.com/gtm.js" />
    </Head>

먼저 next/head 부분에 Partytown이라는 태그를 넣어주면 webworker를 사용할 준비는 마친겁니다.

여기서 forward란 gtm처럼 window.dataLayer에 접근하는 코드가 있을 경우에는 forward에 저렇게 넣어줘야 dataLayer.push 라는것을 인지하고 에러를 뱉지 않습니다.

        <script
          type="text/partytown"
          dangerouslySetInnerHTML={{
            __html: `
              document.getElementById('output-script').textContent = 'passed';
              document.body.classList.add('completed');
            `,
          }}
        />

이렇게 partytown 라이브러리를 이용해서 사용할 수 있을뿐만 아니라 nextjs에서 제공하는 Script태그에도 partytown을 내장하고 있는 문법이 있습니다.

바로 strategy attribute인데요.

nextjs Script docs

별도의 설정없이 Script태그에 strategy="worker"을 주면 이용이 가능합니다.

<Script src="https://example.com" strategy="worker" />

하지만 이것은 app directory와 마찬가지로 실험적인 기능이기 때문에 next.config.js의 experimental옵션에 다음과 같은 설정을 해주어야 합니다.

module.exports = {
  experimental: {
    nextScriptWorkers: true,
  },
};

한계

하지만 partytown의 현재 버전은 0.8.0 베타버전이기도 하며 모든 외부스크립트에 적용이 가능한것은 아닙니다.

특히 내부에 setInterval로직이 있는 그런 스크립트같은 경우 partytown이용시 web worker를 똑바로 못받아오는 문제가 존재합니다.

이 부분만 주의하시면 비용이 큰 스크립트를 따로 처리하는데는 큰 문제가 없을것 같습니다.

🏃🏻'미리' 받아오기 - preload, preconnect

중요도가 높은 스크립트들을 빨리 받아올때 무엇보다 필요한 옵션들입니다.

예를 들어 최대한 빨리 보여져야하는 폰트, 혹은 가장 처음 보이는 뷰에 영향을 끼치는 스크립트라면 최대한 빨리 받아오는 것이 좋겠죠.

그것을 위해서 preconnectpreload를 이용할수 있습니다.

https://beomy.github.io/tech/browser/preload-preconnect-prefetch/

🏃 preconnect - 미리 도메인에 연결하기

preconnect는 외부 도메인에서 특정 스크립트및 css를 받아올때 용이한 옵션입니다.

이 옵션을 설정하면 실제 외부 도메인과 연결할때 미리 필요한 소켓을 연결하기에 실제로 요청을 할때는 dns, tcp시간을 절약할 수있습니다.

예를 들어 https://example.com을 미리 preconnect 설정해놓으면 이곳으로 보내는 요청에 대한것은 미리 소켓을 연결한 상태로 처리하기 때문에 조금 더 빠르게 응답이 이루어질것입니다.

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

🏃 preload - 미리 다운로드 받아버리기

현재 페이지에 필요한 리소스의 우선순위를 높여서 가장 먼저 받아오게 하는 기법입니다.

아까 말했듯이 폰트나 가장 먼저 보이는 뷰에 영향을 끼치는 스크립트에 적용을 하면 좋습니다.

<link rel="preload" src="https://fontfont.com/font" as="font" />

참고로 as에 불러오는 리소스의 속성을 제대로 명시해줘야 리소스를 이용할 수있습니다.

알아두면 좋을것 - prefetch, prerender

이번 프로젝트에 사용한것은 preload, preconnect정도이지만 prefetch, prerender도 최적화하는데 많은 도움이 될수있습니다.

prefetchpre가 받아져있지만 오히려 우선순위를 높인다기 보다는 우선순위를 늦춘다고 생각하시면 됩니다.

미래에 사용할 컴포넌트를 미리 받아오고 후에 캐시에 저장하고 컴포넌트를 받아올시 캐시에 있는 값을 보내주는 역할입니다. 즉, 미래의 사용할 컴포넌트에 이용하시면 됩니다.

<link rel="prefetch" href="about.html">

이렇게 link태그를 이용하여 자원을 미리 받아올 수 있지만 또 webpack에서 import의 형식 주석으로도 표현할 수 있습니다.

import(/* webpackPrefetch: true */ './prefetch.jsx');

prerender는 다음에 이동할 페이지에 대해서 미리 렌더링을 한뒤 그 페이지로 이동할 경우 캐시된 페이지를 보여주는 것입니다.

<link rel="prerender" href="https://future.com">

사용할일이 있으면 적극적으로 활용하는것도 좋지만 두 전략모두 미리 받아와서 캐시에 저장하는것이기때문에 불필요한것을 받아오지 않고 너무 많은 리소스를 미리 받아오지 않도록 조심해야합니다.

캐시라는것도 자원을 사용하는것이기 때문이죠 👀

⚠️ 주의해야할점 ⚠️

하지만 모든 최적화기술에는 어느정도 한계 및 약점이 있기 마련이죠.

preconnect 같은 경우 새 연결을 연다는것 자체가 cpu 리소스를 사용하는 일이기때문에 과도하게 사용하면 좋지 않습니다.

preload 같은 경우에는 필요하지 않는것은 불러오지 않도록 주의해야합니다.

prefetch, prerender는 앞에서 말했듯이 미리 불러놓고 캐시에 저장하는것이기 때문에 불필요한것에 모두 이용하게 된다면 불필요하게 리소스가 많이 사용될것입니다.

👻 비동기로 스크립트 받아오기 - async, defer

async, defer 은 비교적 많이 들어봤을거라고 생각됩니다. 스크립트를 비동기로 받아올때 주로 사용합니다.

동기적으로 받아온다면 용량이 큰 스크립트를 받아올때는 뒤에 받아올 리소스들이 블로킹 되기 때문이죠.

그래서 실제 보이는 화면에 영향을 주는 스크립트같은 경우 미리 로드하고 DomContentLoad이벤트가 발생하면 그때 실행하는 defer, 그게 아닐시 화면과 상관없이 다운로드를 마치면 실행하는 async를 사용하면 됩니다.

이러한 스크립트말고도 css에 대해서도 비동기로 불러올 수 있는데요.

ssr을 이용할시 js같은 경우 늦게 로드가 되더라도 상호작용 시간이 느려질뿐 실제 화면은 보이게 되는데요. 하지만 css파일이 느려질경우에는 렌더트리를 그려지는것자체가 느려지기때문에 첫 html파일자체가 늦게 보이게되어 ssr의 장점이 없어지게됩니다.

css 비동기로 받아오기

useEffect(() => {
    const link = document.createElement('link');
    const handleLoadEvent = function () {
      this.media = 'all';
    };
    handleLoadEvent.bind(link);
    link.rel = 'stylesheet';
    link.href = 'https://www.example.com/reset.css';
    link.media = 'print';
    link.onload = handleLoadEvent;

    document.head.appendChild(link);
}, [])

css를 비동기로 받아오려면 media속성을 활용해야합니다.

media를 print로 하면 css를 다운로드는 하지만 실행을 하지 않습니다. 그런후 media를 all로 하면 다운로드가 완료된 후 실행을 시작합니다!

그런데 react에서 왜 저렇게 링크를 만들고 head에 삽입하는식으로 이용했을까요? 바로 onLoad 이벤트 때문입니다.
실제로 link태그에 onLoad이벤트가 존재한다고 typescript는 알려주지만 실제로 lib.d.ts를 읽어보면 onLoad는 오직 이미지에서만 동작합니다.

그래서 실제 html의 onload이벤트를 이용하기 위해서 다음과 같이 실제 element를 만든뒤 처리하였습니다.

마무리

이번에는 스크립트 로드 최적화에 대해서 알아봤는데요. 이것도 역시 무작정 사용하는것은 지양하는것이 좋습니다.

  • partytown 같은 경우에는 아직 안되는 스크립트가 조금 존재하는 편입니다.
  • prefetch, preload등은 미리 불러오고 캐시에 저장해놓는 방식이기 때문에 너무 많은 요청을 미리 불러오는것은 메모리에 부담을 줄 수 있습니다.
  • async같은 경우 페이지에 독립적인 스크립트에는 사용해도 괜찮지만 페이지의 돔에 종속적인 경우 에러를 뱉을 수도 있습니다. 종속적인 경우 defer를 이용하는게 좋습니다.
  • 비동기로 css를 받아오는 방법은 react의 onLoad이벤트를 이용할수 없기에 real dom의 onload 이벤트를 이용해야 합니다.
  • 비동기로 css를 받아올때는 초반 ui에 영향이 없는 css여야 합니다.
profile
딩구르르

0개의 댓글