[Web.dev] Fast (6) - Resource Delivery Optimization

2022. 7. 3. 18:29Frontend

 

Overview

web.dev에서 소개하는 Web Performance에 대한 내용들을 여러 챕터에 걸쳐서 정리합니다. 모든 내용들을 다 다루지는 않고, 개인적으로 중요하다고 생각하는 부분들을 추려서 중점적으로 정리했습니다. 자세한 내용들은 아래 Table of Contents의 링크를 통해 확인하실 수 있습니다.

Optimize your resource delivery

 

Preload Critical Assets to Improve Loading Speed

사전 로딩(Preload)은 일반적으로 브라우저가 늦게 발견한 리소스에 가장 적합합니다. 

 

CSS안에 명시된 background-image나 font-face와 같은 리소스들은 브라우저가 처음 페이지의 HTML 문서를 로드하고, 해당 문서를 파싱 하면서 CSS파일을 찾아 요청하고, 이 도착한 CSS파일을 파싱 하다가 해당 리소스를 발견하면 그때서야 로드됩니다. 따라서 해당 리소스들이 화면의 첫 렌더 때 사용자에게 무조건 보여야 한다면 해당 리소스들은 미리 로드하는 것이 좋습니다. 

 

 

Critical CSS

Critical CSS는 사용자에게 가능한 한 빨리 컨텐츠를 렌더링 하기 위해 스크롤 없이 볼 수 있는 콘텐츠에 대한 CSS를 추출하는 기술입니다. 아래 그림처럼 실제로 스크롤 없이 사용자의 화면에 보이는 콘텐츠는 "ABOVE THE FOLD" 구간에 위치한 내용들입니다. 사용자 경험을 최적화하기 위해서는 사용자가 가장 빨리 첫 화면을 볼 수 있게 "ABOVE THE FOLD"에 해당하는 CSS를 Critical CSS로 분류하고, "BELOW THE FOLD"에 해당하는 CSS는 사전 로드하여 해당 CSS가 필요할 때 사용자에게 지연 없이 화면을 그릴 수 있도록 하는 것이 좋습니다. 

 

 

 

 

 

 

https://web.dev/extract-critical-css/

 

중요한 CSS 추출

중요한 CSS 기술로 렌더링 시간을 개선하는 방법과 프로젝트에 가장 적합한 도구를 선택하는 방법에 대해 알아보세요.

web.dev

 

 

Fast Playback with Audio and VIdeo Preload

Preload에 대해 제대로 이해하기 위해서는 Browser에서 DOM을 다룰 때 발생시키는 이벤트인 DOMContentLoaded와 Load 이벤트를 이해해야 합니다. 따라서 관련 섹션을 아래에 추가하고 이전에 해당 내용에 대해 다루었던 포스팅을 소개합니다.

 

Preload, Prefetch and Preconnect

Preload, Prefetch, Preconnect에 대한 개념 설명은 이전에 해당 내용을 소개한 포스팅에서 자세히 확인하실 수 있습니다.

 

[Web] preload, prefetch, preconnect

Overview 아무리 복잡한 웹 애플리케이션이라도 모든 것은 HTML 문서를 로딩하는 것부터 시작됩니다. 로딩된 문서는 자바스크립트를 실행하기 위한 태그, CSS style이나 image 등을 가져오기 위한 태그

yeoulcoding.me

 

 

Video Preload

기본적으로 preload 속성은 브라우저에게 "힌트"를 제공하는 것입니다. 이 힌트를 사용해서 올바르게 리소스를 preload하는 것은 전적으로 브라우저의 책임이며, 이 때문에 동작을 100% 보장할 수 없습니다. 해당 기능을 "반드시" 사용해야 하는 경우 공식 문서에서 지원 브라우저 목록을 확인하는 것이 좋습니다. 

https://developer.mozilla.org/ko/docs/Web/HTML/Element/Video#attr-preload

 

<video>: 비디오 삽입 요소 - HTML: Hypertext Markup Language | MDN

HTML <video> 요소는 비디오 플레이백을 지원하는 미디어 플레이어를 문서에 삽입합니다.

developer.mozilla.org

 

모바일 환경에서 주의할 점 (크롬 기준)

"Light Data Mode(데이터 절약모드)"가 활성화된 경우, Chrome은 강제로 preload 값을 none으로 설정하여 비디오가 사전 로드되지 못하도록 처리합니다. 또한 "Cellular Connection"에서 Chrome은 강제로 preload 값을 metadata로 설정합니다. 따라서 모바일 브라우저를 지원하거나 웹뷰를 지원하는 경우 이를 고려한 에셋 관리 및 UX설계를 하는 것이 좋습니다.

 

 

Tips

HTTP 1.1 Spec하에서 Chrome 기준 브라우저가 동일한 도메인에 대해서 맺을 수 있는 Maximum Connection 수는 6개입니다. 따라서 사전 로딩하려는 동영상이 Critical User Experience에 해당하는 리소스가 아니라면 poster를 정의하고 preload를 none이나 metadata로 설정하는 것이 좋습니다.

 

How next/link Works

https://nextjs.org/docs/api-reference/next/link

 

next/link | Next.js

Enable client-side transitions between routes with the built-in Link component.

nextjs.org

prefetch - Prefetch the page in the background. Defaults to true. Any <Link /> that is in the viewport (initially or through scroll) will be preloaded. Prefetch can be disabled by passing prefetch={false}. When prefetch is set to false, prefetching will still occur on hover. Pages using Static Generation will preload JSON files with the data for faster page transitions. Prefetching is only enabled in production.

 

Nextjs는 Link 컴포넌트를 구현할 때, 내부적으로 next/router를 사용합니다. next/link를 사용하면 해당 링크를 사용한 컴포넌트가 렌더링 되면 조건에 따라 유저가 해당 링크를 방문하지 않아도 참조하고 있는 페이지의 번들을 prefetch 하는데, 이는 내부적으로 IntersectionObserver와 next/router를 통해 구현됩니다.

 

https://github.com/vercel/next.js/blob/canary/packages/next/client/link.tsx

 

GitHub - vercel/next.js: The React Framework

The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.

github.com

...
const {
  href: hrefProp,
  as: asProp,
  children: childrenProp,
  prefetch: prefetchProp,
  passHref,
  replace,
  shallow,
  scroll,
  locale,
  onClick,
  onMouseEnter,
  ...restProps
} = props

children = childrenProp

if (legacyBehavior && typeof children === 'string') {
  children = <a>{children}</a>
}

const p = prefetchProp !== false
const router = useRouter() 
...

 

 

next/link의 prefetch가 동작하는 상세한 원리는 다음과 같습니다. 구현 코드는 아래를 참고해주세요.

 

  1. prefetch 의 조건은 isVisible, p, isLocalUrl(href)의 조합으로 결정되고, 해당 조건이 충족되면 prefetch 함수를 호출합니다.

    1. prefetch 함수는 next/router의 prefetch를 사용하고, prefetch된 path는 nextjs가 내부적으로 map을 사용하여 메모리에 캐시 합니다.

    2. next/router의 prefetch를 사용하면 해당 페이지의 번들을 사전에 가져와 브라우저의 캐시에 저장합니다.

  2. p는 사용자가 next/link 컴포넌트에 제공한 prefetch 옵션을 의미합니다. 해당 옵션에 false를 주었다면 prefetch는 동작하지 않습니다.

  3. isVisible은 내부적으로 IntersectionObserver를 사용하여 트리거 됩니다. 만약 브라우저가 IntersectionObserver를 지원하지 않는 경우 isVisible은 항상 false가 됩니다.

  4. isLocalUrl은 same-origin을 의미합니다. 따라서 cross-origin에서의 link는 prefetch를 하지 않습니다
React.useEffect(() => {
  const shouldPrefetch = isVisible && p && isLocalURL(href)
  const curLocale =
    typeof locale !== 'undefined' ? locale : router && router.locale
  const isPrefetched =
    prefetched[href + '%' + as + (curLocale ? '%' + curLocale : '')]
  if (shouldPrefetch && !isPrefetched) {
    prefetch(router, href, as, {
      locale: curLocale,
    })
  }
}, [as, href, isVisible, locale, p, router])

...

function prefetch(
  router: NextRouter,
  href: string,
  as: string,
  options?: PrefetchOptions
): void {
  if (typeof window === 'undefined' || !router) return
  if (!isLocalURL(href)) return
  // Prefetch the JSON page if asked (only in the client)
  // We need to handle a prefetch error here since we may be
  // loading with priority which can reject but we don't
  // want to force navigation since this is only a prefetch
  router.prefetch(href, as, options).catch((err) => {
    if (process.env.NODE_ENV !== 'production') {
      // rethrow to show invalid URL errors
      throw err
    }
  })
  const curLocale =
    options && typeof options.locale !== 'undefined'
      ? options.locale
      : router && router.locale

  // Join on an invalid URI character
  prefetched[href + '%' + as + (curLocale ? '%' + curLocale : '')] = true
}

...

export function isLocalURL(url: string): boolean {
  // prevent a hydration mismatch on href for url with anchor refs
  if (url.startsWith('/') || url.startsWith('#') || url.startsWith('?'))
    return true
  try {
    // absolute urls can be local if they are on the same origin
    const locationOrigin = getLocationOrigin()
    const resolved = new URL(url, locationOrigin)
    return resolved.origin === locationOrigin && hasBasePath(resolved.pathname)
  } catch (_) {
    return false
  }
}
반응형