Lighthouse Render-blocking requests: 해결 방법

Lighthouse 13은 첫 페인트 전에 페이지가 기다리는 모든 스타일시트와 스크립트를 나열합니다. 빠른 파일도 포함됩니다. 이러한 요청이 방문자의 시간을 소모시키는 시점과 실제로 효과가 있는 CSS 해결 방법을 다룹니다.

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-10-17

render-blocking 요청이 점수를 떨어뜨릴까요?

Lighthouse는 첫 페인트 전에 페이지가 기다린 모든 스타일시트와 스크립트를 Render-blocking requests 인사이트("요청이 페이지의 초기 렌더를 차단하여 LCP가 지연될 수 있습니다", Lighthouse 13 이전에는 "Eliminate render-blocking resources")에 나열합니다. 이 인사이트 자체가 점수에 직접 반영되지는 않습니다. 하지만 페이지가 CSS와 스크립트를 기다리는 동안 화면은 흰색으로 유지되며, 특히 모바일에서 FCP와 LCP가 지연됩니다. CSS를 정리하고 병합하며 스크립트는 defer하세요.

Render-blocking requests 인사이트는 무엇을 점검할까요?

Lighthouse 설명은 이렇습니다. "요청이 페이지의 초기 렌더를 차단하여 LCP가 지연될 수 있습니다. 지연하거나 인라인 처리하면 이러한 네트워크 요청을 크리티컬 패스 밖으로 옮길 수 있습니다." 하지만 인라인 처리는 해결책이 아닙니다. 아래 내용을 참고하세요. 표에는 URL, Transfer Size, Duration 세 개 열이 있습니다. Transfer Size는 압축되어 전송된 파일 크기입니다. Duration은 다른 도메인으로의 새 연결 시간을 포함해, 느린 모바일 기기에서 요청에 걸리는 시간을 Lighthouse가 추정한 값입니다.

Chrome은 페이지를 로드하면서 모든 요청에 render-blocking 여부를 표시하고 이를 trace에 기록합니다. Lighthouse는 이 trace를 읽어 Chrome이 차단으로 표시하고 첫 페인트 전에 완료된 메인 프레임의 모든 요청을 나열합니다. 실제로는 <head> 내부의 스타일시트와 동기 스크립트, 그리고 이들이 @import로 불러오는 파일들이 여기에 해당합니다. body의 스타일시트는 Chrome의 highest 우선순위일 때만, 스크립트는 high 우선순위일 때만 포함됩니다. 첫 번째 이미지 이후에는 둘 다 우선순위가 떨어져 제외됩니다. 기준치는 없습니다. 파일이 하나라도 목록에 오르면 인사이트 통과에 실패합니다.

페이지 속도를 떨어뜨리더라도 목록에 전혀 나타나지 않는 파일도 있습니다. async 스크립트, 스크립트가 페이지에 추가하는 파일(gtm.js, gtag.js), 인라인 스타일과 스크립트, iframe 내부의 모든 리소스, 그리고 폰트가 여기에 해당합니다.

Render-blocking requests 인사이트를 펼친 Lighthouse 13.5.0 리포트: 예상 절감 시간 1,140 ms, FCP, LCP, Unscored 라벨, URL, Transfer Size, Duration이 표시된 12개 요청, 비어 있는 몇몇 Duration 셀

WooCommerce 상품 페이지의 인사이트 화면. Lighthouse 13.5.0, PageSpeed Insights와 동일한 시뮬레이션 throttling 적용.

무엇이 파일을 render-blocking으로 만들까요?

Render-blocking이란 해당 파일이 도착할 때까지 브라우저가 아무것도 그리지 못함을 뜻합니다. <head> 안의 모든 스타일시트가 도착해야 하고, 모든 동기 스크립트가 다운로드되어 실행되어야 합니다.

  • 스타일시트 뒤에 있는 스크립트는 해당 스타일시트를 기다립니다. Chrome은 상위 CSS가 도착할 때까지 동기 스크립트를 실행하지 않고, 파서는 스크립트 실행을 기다립니다. 두 파일을 나란히 다운로드하더라도 CSS를 기다린 후 스크립트까지 기다려야 합니다.
  • @import는 늦게 시작됩니다. Chrome의 preload 스캐너는 HTML에 있는 모든 링크를 한 번에 찾아내지만, CSS 파일 내부의 @import는 해당 CSS 파일이 도착한 뒤에야 발견합니다. 아래 상품 페이지에서 HTML로 직접 연결했을 때는 테마 스타일시트 로드가 0.59초에 시작했지만, import된 스타일시트는 1.28초에 시작했습니다.
  • 외부 도메인은 연결 비용이 추가됩니다. fonts.googleapis.com이나 CDN의 스타일시트는 첫 바이트가 도착하기 전에 DNS, TCP, TLS 과정을 거쳐야 합니다.
Chrome 동작첫 페인트를 지연시키는가?
head 내부의 <link rel="stylesheet">예
fetchpriority="low"를 지정한 동일 링크예
media="print"를 지정한 동일 링크아니요(다운로드는 진행됨)
동일 파일에 대한 rel="preload" 및 스타일시트 링크예
차단 스타일시트 내부의 @import예(부모 파일 이후)
head 내부의 <script src>예
async아니요(단, 페인트 전에 실행될 수 있음)
defer, type="module"아니요
body 내부의 스타일시트해당 요소 하위 콘텐츠만

render-blocking 요청이 FCP와 LCP에 영향을 미칠까요?

head에 스타일시트 9개와 스크립트 2개가 포함된 WooCommerce 상품 페이지를 대상으로 Slow 4G 모바일 환경에서 직접 측정해 보았습니다. 개선된 버전에서는 CSS를 정리해 6.6 kB 크기의 단일 스타일시트로 병합했고, 두 스크립트 모두 defer 처리했습니다. 로드 후 화면은 이전과 완전히 동일합니다.

FCP

기존 버전의 첫 페인트는 2.12초였고, 개선된 버전은 1.39초였습니다.

개선 전 1.4초 시점의 모바일 상품 페이지: 흰 화면

개선 전

개선 후 1.4초 시점의 모바일 상품 페이지: 스타일이 적용된 페이지가 표시되고 상품 사진은 아직 로딩 중

개선 후

스로틀링이 적용된 모바일 기기에서 1.4초 시점의 두 버전: 기존 버전은 여전히 흰 화면입니다.

LCP

LCP는 첫 페인트보다 먼저 발생할 수 없습니다. 이 페이지에서 LCP 요소는 상품 사진이며, CSS가 로드되기 전에는 사진 역시 나타날 수 없습니다. 개선 작업을 통해 LCP는 2.25초에서 1.91초로 단축되었습니다. LCP 세부 분석에서는 이러한 대기 시간이 element render delay로 표시됩니다. Lighthouse는 이를 보여주지 않습니다. LCP 요소가 이미지인 경우 LCP 절감 시간을 항상 0 ms로 추정하므로, LCP에 영향이 없다고 오해해서는 안 됩니다.

얼마나 많은 사이트가 영향을 받으며, 무시해도 괜찮을까요?

거의 모든 사이트가 영향을 받습니다. CrUX 데이터를 보유한 114,526개 사이트의 홈페이지를 조사한 State of CWV 데이터에 따르면, 92.8%에 최소 하나 이상의 render-blocking 요청이 있었고, 91.2%는 차단 스타일시트, 62.5%는 차단 스크립트를 가지고 있었습니다. 이러한 요청이 존재하는 사이트의 중앙값은 8개 요청에 110 kB였으며, 사이트 다섯 곳 중 한 곳은 20개가 넘었습니다. (State of CWV는 head의 스타일시트와 동기 스크립트를 집계하므로 Lighthouse 목록과 거의 일치합니다.)

이러한 요청이 많은 사이트일수록 field data에서 성적이 나쁩니다. 차단 요청이 전혀 없는 사이트는 모바일에서 84.0%가 양호한 FCP를 기록한 반면, 20개가 넘는 사이트는 54.1%에 그쳤습니다. 하지만 이 하락 폭의 대부분은 서버 성능 차이에서 기인합니다. 같은 구간에서 중앙값 TTFB가 538 ms에서 1,114 ms로 두 배 이상 증가했기 때문입니다. 서버 속도별로 나누어 보면 다음과 같습니다.

차트: render-blocking 요청 수에 따른 모바일 양호 FCP 사이트 비율. 빠른 서버(녹색 막대)는 95% 안팎을 유지하고, 느린 서버(빨간색 막대)는 21개 이상에서 30%까지 급감

render-blocking 요청 수와 서버 응답 시간에 따른 모바일 양호 FCP 사이트 비율. State of CWV, 2026년 10월, 모바일 CrUX p75, 양호한 FCP 기준 1.8초 이하.

  • 빠른 서버(TTFB 600 ms 미만): 요청 개수와 관계없이 94~97%가 양호한 FCP를 기록합니다. 그렇다고 지연이 없는 것은 아닙니다. 차단 요청이 21개 이상인 경우 전혀 없는 곳보다 첫 바이트부터 첫 페인트까지 약 150~200 ms가 더 걸립니다.
  • 느린 서버(TTFB 900 ms 이상): 양호한 FCP를 기록하는 사이트 비율이 차단 요청 2개 이하일 때 약 절반(47~53%)에서 21개 이상일 때 30%로 떨어집니다. TTFB가 900 ms 이상이면 양호한 FCP 기준선인 1.8초까지 0.9초도 채 남지 않으므로, 페인트가 지연되는 요청이 하나씩 추가될 때마다 FCP 기준치를 넘겨버릴 수 있습니다.

WordPress 사이트는 이러한 요청이 더 많습니다. 57%가 16개 이상의 차단 요청을 가지고 있습니다(중앙값 19개, 163 kB). 동일한 서버 속도 기준으로 빠른 호스트에서는 요청이 1~5개일 때보다 양호한 FCP 점유율이 3%p 하락하고, 느린 호스트에서는 약 10%p 하락합니다.

이는 상관관계지만 명확한 우선순위를 보여줍니다. TTFB가 느리다면 서버부터 고치세요. 빠른 서버라면 직접 작성한 CSS를 최적화해 150~200 ms를 줄일 가치가 충분합니다. 다음의 경우에는 해당 알림을 무시해도 괜찮습니다.

  • Lighthouse에 표시된 파일이 직접 작성한 15 kB 미만의 작은 스타일시트 한두 개뿐인 경우
  • 목록에 있는 파일이 동의 관리 도구, A/B 테스트 스니펫, 또는 직접 수정할 수 없는 플랫폼 파일인 경우(아래 내용 참고)

field data에서 render-blocking 요청을 어떻게 찾아낼까요?

Lighthouse는 캐시가 없는 상태(cold)로 페이지를 한 번 로드합니다. 실제 방문자는 첫 페이지 방문 이후 CSS를 캐시에 저장하므로, field에서는 방문자가 처음 방문하는 페이지에서 주로 이 비용이 드러납니다. CoreDash의 모바일 화면에서 두 가지를 확인하세요.

  • TTFB 옆의 FCP. Render-blocking 파일은 첫 바이트와 첫 페인트 사이의 시간을 소모합니다. 다른 템플릿보다 이 간격이 큰 템플릿일수록 차단 파일이 더 많이 로드되고 있는 것입니다.
  • LCP render delay. LCP에서 render delay 비중이 큰 템플릿을 찾으세요. 차단 CSS는 이러한 지연의 대표적인 원인 중 하나입니다. 해당 페이지에 차단 파일이 있는지는 DevTools(다음 섹션)에서 확인할 수 있습니다.

CrUX도 오리진이나 특정 URL에 대한 p75 기준 동일한 지표를 제공하지만, 어느 템플릿이 느린지는 알려주지 않습니다.

모바일 CoreDash 화면: URL별 p75 First Contentful Paint, 황색으로 표시된 상품 및 카테고리 페이지

샘플 쇼핑몰(acme-pagespeed.com)의 CoreDash 모바일 p75 화면: 상품 페이지와 카테고리 페이지가 느립니다.

DevTools에서 차단 파일을 어떻게 찾을까요?

Chrome에서 페이지를 열고 Performance 패널로 이동한 뒤, 네트워크를 Slow 4G로, CPU를 4x slowdown으로 설정하고 새로고침을 기록합니다. Insights 사이드바에 Render-blocking requests 항목이 표시됩니다. 이를 클릭하면 DevTools가 Network 트랙에서 각 차단 요청을 강조 표시합니다.

Render-blocking requests 인사이트가 열려 있는 Chrome DevTools Performance 패널. Network 트랙에 차단 요청이 윤곽선으로 강조되고 마지막 요청 뒤에 FCP 마커가 위치함

각 막대는 요청이 시작된 순간부터 마지막 바이트가 도착할 때까지의 구간을 나타냅니다. FCP 마커 직전에 가장 늦게 끝나는 요청을 찾으세요. 페인트는 바로 그 요청을 기다린 것입니다. 이전 요청이 끝나야만 시작되는 CSS 요청은 @import입니다. 차단하는 요청이 없으면 사이드바에 "No render-blocking requests for this navigation"이라고 표시됩니다.

render-blocking 요청은 어떻게 해결할까요?

Lighthouse 표의 URL을 보면 각 파일의 출처를 알 수 있으며, 이에 따라 수정할 위치가 결정됩니다.

직접 작성한 CSS와 스크립트

직접 관리하는 템플릿이라면 다음과 같은 head 구성을 목표로 하세요. 최적화된 작은 스타일시트 하나만 두고 모든 스크립트는 defer 처리하는 것입니다. 그러면 첫 페인트는 단 하나의 파일만 기다리게 됩니다.

<link rel="stylesheet" href="/css/main.min.css">
<script src="/js/jquery.min.js" defer></script>
<script src="/js/gallery.min.js" defer></script>

구체적인 방법은 아래 CSS 정리 및 병합부터 설명합니다.

WordPress

/wp-content/plugins/ 하위 파일은 플러그인, /wp-content/themes/ 하위는 테마, /wp-includes/ 하위는 WordPress 자체 파일입니다.

직접 코드를 작성하지 않는다면 최적화 플러그인을 사용하고 다음과 같이 설정하세요.

  • 스크립트 defer 활성화: WP Rocket의 Load JavaScript deferred, Perfmatters의 Defer JavaScript, LiteSpeed Cache의 Load JS Deferred를 켜세요. defer된 스크립트는 더 이상 첫 페인트를 막지 않습니다. jQuery는 주의가 필요하니 아래 내용을 참고하세요.
  • Load CSS Asynchronously 및 critical CSS는 끄기: 이 인사이트 아래에 플러그인이 "inline critical assets"한다는 과거의 WordPress 팁이 적혀 있더라도 켜지 마세요. Critical CSS가 동작하기는 하지만, 나머지 CSS가 첫 페인트 이후에 도착하므로 페이지 스타일이 두 번 적용되면서 레이아웃 이동이 자주 발생합니다. 게다가 인라인 처리가 초래하는 추가 비용은 CSS 정리 및 병합에서 다룹니다.
  • Remove Unused CSS는 끄기(LiteSpeed Cache의 Generate UCSS): URL마다 별도의 CSS를 생성하고(WP Rocket은 이를 HTML 안에 인라인으로 삽입) 로드 시점에 페이지에서 발견된 규칙만 유지하므로, 이후 스크립트가 변경하는 요소(열린 메뉴, 슬라이더 등)의 스타일이 깨질 수 있습니다. WP Rocket에는 이로 인한 화면 깨짐 문제만 다룬 전용 가이드가 있을 정도입니다. 얻는 것보다 골칫거리가 더 많습니다. 스타일시트는 직접 정리하고 병합하세요. 손은 더 가지만 충분한 가치가 있습니다.

많은 플러그인이 실제 동작하지 않는 페이지를 포함해 모든 페이지에 CSS를 로드합니다. 폼이 없는 페이지의 Contact Form 7, 쇼핑몰 영역 밖의 WooCommerce가 대표적입니다. WordPress가 출력하는 모든 스타일시트에는 contact-form-7-css와 같은 id가 붙습니다. -css 앞부분이 핸들이며, 이를 사용해 사용되지 않는 페이지에서 제거할 수 있습니다.

add_action( 'wp_enqueue_scripts', function () {
  if ( ! is_page( 'contact' ) ) {
    wp_dequeue_style( 'contact-form-7' );
  }
}, 20 );

WooCommerce는 자체 3개 스타일시트를 제어하는 전용 필터인 woocommerce_enqueue_styles를 제공합니다. 직접 작성한 스크립트의 경우 WordPress 6.3 이상부터 defer를 추가할 수 있습니다.

wp_enqueue_script( 'acme-gallery', get_stylesheet_directory_uri() . '/js/gallery.min.js', array( 'jquery' ), '2.3.0', array( 'strategy' => 'defer' ) );

내 스크립트에 의존하는 다른 스크립트가 여전히 차단 상태이거나 뒤이어 인라인 스크립트가 추가되면, WordPress는 아무런 경고 없이 이 defer 설정을 취소해 버립니다. 페이지 소스에서 script 태그를 직접 확인하세요.

jQuery는 인라인 jQuery(...) 호출까지 함께 처리해 주는 도구를 통해서만 defer해야 합니다. WordPress는 jQuery를 defer 없이 등록하므로 다른 스크립트가 의존하는 즉시 head를 차단하며, 직접 수동으로 defer를 추가하면 그보다 먼저 실행되는 모든 인라인 jQuery(...) 호출이 깨집니다. WP Rocket의 Load JavaScript deferred는 jQuery를 defer하고 인라인 호출을 래핑해 줍니다. Perfmatters의 Defer JavaScript는 Include jQuery 옵션을 켜기 전까지 jQuery를 제외합니다. 이 옵션을 켰다면 페이지 구석구석(메뉴, 갤러리, 장바구니 등)을 클릭해 깨진 곳이 없는지 점검하세요.

style.css 시작 부분에 @import url("../storefront/style.css")가 있는 자식 테마는 @import 체인을 만듭니다. 해당 라인을 삭제하고 wp_enqueue_style로 부모 스타일시트를 로드하세요.

Shopify

앱은 app embeds(Online Store, Edit theme, App embeds)를 통해 파일을 추가합니다. embed의 스크립트 파일은 (Liquid가 직접 script 태그를 작성하지 않는 한) async로 로드되지만, 스타일시트는 차단합니다. 사용하지 않는 embed는 비활성화하고, 앱을 삭제한 뒤에는 theme.liquid와 snippet 파일에서 앱이 남겨둔 잔여 코드가 없는지 검색해 정리하세요.

테마는 theme.liquid에서 stylesheet_tag 필터를 사용해 CSS를 로드하며, 이는 차단 링크를 생성합니다. 기본 베이스 스타일시트 하나 정도라면 괜찮습니다. 반면 script_tag 필터는 defer가 없는 스크립트를 생성하므로, 해당 태그는 다음과 같이 직접 작성하세요.

<!-- blocks the first paint -->
{{ 'gallery.js' | asset_url | script_tag }}

<!-- doesn't -->
<script src="{{ 'gallery.js' | asset_url }}" defer></script>

Shopify는 head에 있는 최대 10개의 render-blocking 파일에 대해 이미 103 Early Hints를 전송하므로 해당 파일들은 어차피 일찍 시작됩니다. 여기서 개선할 수 있는 부분은 파일의 개수와 크기를 줄이는 것입니다. accelerated checkout 스타일시트는 계속 목록에 남아 있을 가능성이 높습니다(아래 내용 참고).

Next.js

App Router에서는 모든 CSS import가 head 상단의 스타일시트 링크로 변환되며, React는 이 파일이 로드될 때까지 페인트를 시작하지 않습니다. Next.js가 이들을 몇 개의 청크로 병합해 주므로, 해결책은 CSS 양 자체를 줄이는 것입니다. 특정 라우트에서만 사용하는 CSS는 글로벌 CSS에서 CSS Modules로 옮기세요. 루트 레이아웃에서 import된 글로벌 CSS는 모든 라우트로 전송되지만, CSS Module은 이를 import한 라우트에만 전송됩니다. 자세한 내용은 Next.js의 CSS 파일 빌드 방식을 참고하세요.

Next.js는 자체 스크립트를 async나 defer로 로드합니다. Next.js 사이트에 차단 스크립트가 있다면 누군가 레이아웃에 직접 수동으로 추가한 것입니다. next/script를 사용해 로드하세요. 기본적으로 Next.js가 페이지를 hydration하기 시작한 후에 로드됩니다.

import Script from 'next/script'

<Script src="https://example.com/widget.js" strategy="afterInteractive" />

Google Fonts 및 기타 도메인

Google Fonts 링크는 fonts.googleapis.com에서 가져오는 차단 스타일시트이며, 폰트 파일 자체는 또 다른 도메인인 fonts.gstatic.com에서 전송됩니다. Lighthouse는 이 0.5 kB짜리 파일에 가장 긴 Duration을 부여했는데, 새로운 연결 비용을 계산에 포함하기 때문입니다. 폰트를 직접 호스팅하고 자체 스타일시트에 @font-face 규칙을 넣으세요. 그러면 이 요청과 두 도메인으로의 연결이 모두 사라집니다. Next.js에서는 next/font가 이미 이 작업을 수행합니다.

동의 관리 도구, A/B 테스트 및 건드리지 말아야 할 파일들

  • 자동 차단 기능이 있는 동의 관리 도구(Cookiebot, OneTrust의 OtAutoBlock, Usercentrics 오토블로커): 다른 모든 스크립트를 제어하려면 그보다 먼저 실행되어야 하므로 구조상 동기식으로 동작합니다. 대안으로는 모든 쿠키 스크립트를 직접 마크업하는 수동 모드가 있습니다. 이것이 그만한 가치가 있는지는 우선 법적 검토가 필요한 문제입니다. 해당 도구의 도메인에 preconnect를 설정하면 최소한 연결을 일찍 시작할 수는 있습니다.
  • A/B 테스트 스니펫(Optimizely, AB Tasty, Convert): 변형 페이지가 적용되기 전에 원본 페이지가 깜빡이며 노출되는 현상을 방지하기 위해 의도적으로 동기식으로 작동합니다. 진행 중인 테스트가 없을 때는 제거하세요.
  • Shopify의 content_for_header: 모든 테마에 반드시 필요하며 수정할 수 없습니다. 이 글을 위해 조사한 5개 쇼핑몰 모두에서 accelerated checkout용 차단 스타일시트인 accelerated-checkout-backwards-compat.css가 추가되었습니다. 그대로 두세요.

CSS 정리 및 병합

render-blocking CSS는 압축 기준 각각 약 15 kB 미만인 몇 개의 스타일시트로 줄이세요. 15 kB는 새 연결의 첫 번째 라운드 트립에서 전달할 수 있는 대략적인 크기입니다.

Slow 4G 환경의 Chrome DevTools Network 폭포수: 대기 시간이 거의 전부인 0.3 kB 스타일시트 4개, 짧은 다운로드 시간의 14 kB 스타일시트, 다운로드 시간이 긴 60 kB 스타일시트

DevTools Network 패널에서 본 Slow 4G 환경의 render-blocking 스타일시트 6개. 녹색은 첫 바이트 대기 시간으로 모든 파일이 동일합니다. 파란색은 다운로드 시간으로 파일 크기가 클수록 길어집니다.

  • 테마와 플러그인이 추가하는 자잘한 파일을 병합하세요. 모든 스타일시트는 크기와 관계없이 첫 바이트가 오기까지 전체 왕복 시간(Slow 4G 기준 약 0.6초)을 대기해야 합니다. 0.3 kB짜리 파일 하나 때문에 아무것도 아닌 일에 온전한 대기 시간을 치르는 셈입니다.
  • 큰 파일은 줄이거나 나누세요. 동일한 대기 시간 후 14 kB 파일은 0.4초 만에 도착했습니다. 하지만 60 kB 파일은 다운로드에 1초가 더 필요했고, 그 파일이 도착할 때까지 화면은 하얗게 멈춰 있었습니다.
  • 페이지에서 쓰지 않는 규칙은 제거하세요. DevTools의 Coverage 탭을 보면 각 파일에서 사용되지 않는 CSS의 비율을 확인할 수 있습니다. 사이트들의 중앙값 기준 무려 89%가 미사용 코드입니다(State of CWV, 홈페이지 기준 Chrome coverage).
  • 모든 CSS를 하나의 거대한 파일로 합치지 마세요. 정리하지 않은 전체 CSS를 68 kB짜리 단일 파일로 묶었더니 9개의 파일로 나뉘어 있을 때보다 첫 페인트가 더 늦어졌습니다. 큰 파일 하나가 대역폭을 defer된 스크립트 및 히어로 이미지와 나눠 쓴 반면, 9개 파일일 때는 render-blocking 요청들이 대역폭의 대부분을 먼저 차지했기 때문입니다.
  • CSS를 인라인 처리하지 마세요. 인라인 CSS는 HTML 안에 들어갑니다. HTML은 브라우저에 가장 먼저 필요한 파일이므로 다른 모든 리소스가 HTML을 기다립니다. 따라서 인라인 처리는 모두가 기다리는 단 하나의 파일을 더 무겁고 느리게 만들 뿐입니다. 더 중요한 점은 브라우저가 대개 HTML을 캐시하지 않는다는 사실입니다. 별도 스타일시트는 첫 페이지 로드 후 캐시되지만, 인라인 CSS는 페이지를 볼 때마다 매번 다시 다운로드됩니다. TTFB가 900 ms 미만이고 차단 요청이 1~5개인 사이트 중 인라인 CSS가 없는 사이트는 95.9%가 양호한 FCP를 기록한 반면, 300 kB 이상을 인라인 처리한 사이트는 90.8%에 불과했습니다(State of CWV).
  • "나머지" CSS를 나중에 로드하려고 분리하지 마세요. 과거에 쓰이던 media="print" 꼼수는 첫 화면에 절대 나타나지 않는 CSS에만 통합니다. 하지만 모든 페이지와 모든 화면 크기에서 이를 완벽히 파악하기란 불가능에 가깝습니다. 예측이 빗나가면 스타일이 적용되지 않은 민낯의 페이지가 먼저 렌더링됩니다.

모든 @import 대체하기

import된 파일마다 HTML에 별도의 link 태그를 지정하거나, 부모 파일에 병합하세요. 스크립트를 defer한 상태에서 상품 페이지의 @import를 제거했더니 첫 페인트가 137 ms 앞당겨졌습니다. 스크립트가 여전히 차단 상태였을 때는 33 ms 절감에 그쳤습니다. jQuery가 import된 스타일시트와 거의 같은 시점에 도착해, 어느 쪽이든 페인트가 jQuery를 기다려야 했기 때문입니다.

스크립트 defer 처리

첫 페인트에 필요하지 않은 head 내부의 모든 스크립트에 defer를 추가하세요. 모듈 스크립트는 이미 defer 처리되어 있습니다. async 역시 스크립트를 목록에서 제외해 주지만, 첫 페인트 전에 실행되어 main thread를 점유할 수 있으므로 defer가 더 나은 기본값입니다. 요즘 이 인사이트에서 스크립트가 차지하는 비중은 비교적 작습니다. CSS가 어차피 더 늦게 끝났기 때문에 두 스크립트를 모두 defer해도 단축된 시간은 24 ms에 불과했습니다. 자세한 내용은 async vs defer, head vs footer JavaScript, JavaScript를 defer하는 14가지 방법을 참고하세요.

103 Early Hints로 메인 스타일시트 일찍 시작하기

103 Early Hints를 사용하면 서버가 HTML을 생성하는 동안 브라우저에 어떤 파일을 미리 가져와야 할지 알려줄 수 있습니다. 메인 스타일시트에 힌트를 제공하세요.

HTTP/2 103
link: </css/main.min.css>; rel=preload; as=style

서버가 페이지를 생성하는 데 시간이 걸릴 때 효과가 있습니다. 그 시간 동안 스타일시트가 다운로드되므로 얻을 수 있는 이득은 최대 서버의 연산 시간 수준이며, CDN 캐시에서 제공되는 페이지는 이득이 거의 없습니다. Cloudflare에는 관련 토글 옵션(Speed, Settings, Content Optimization)이 있으며, 서버가 보내는 Link 헤더를 바탕으로 힌트를 생성합니다. Apache 2.4.58 이상 버전에는 H2EarlyHint가 있습니다.

적용 후에는 LCP를 확인하세요. CSS가 일찍 도착하면 CSS에 지정된 폰트 역시 일찍 다운로드를 시작합니다. 해당 상품 페이지에서는 156 kB 크기의 Font Awesome 폰트가 상품 사진의 대역폭을 빼앗아 오히려 LCP가 더 늦어졌습니다. Lighthouse는 힌트가 적용된 파일도 0.0 KiB로 표시하며 목록에 계속 남겨둡니다. 자세한 내용은 103 Early Hints와 이미 연결된 스타일시트를 단순 preload하는 것이 도움이 되지 않는 이유를 참고하세요.

스타일시트에 fetchpriority="low"를 사용하지 마세요

첫 화면에 필요하지 않은 CSS에 fetchpriority="low"를 적용하라는 팁이 자주 보입니다. 이는 안티패턴입니다. Chrome은 head 안에서 해당 스타일시트의 우선순위를 highest에서 high로 한 단계만 낮출 뿐, 첫 페인트 전에 이를 기다리는 것은 여전합니다. 반면 Lighthouse는 시뮬레이션에서 우선순위로 차단 여부를 판단하기 때문에, 파일이 사라진 것처럼 취급하여 Duration도 표시하지 않고 시뮬레이션 FCP를 더 빠르게 계산합니다.

스타일시트 4개에 이를 적용해 보았습니다. 시뮬레이션 throttling을 적용한 Lighthouse에서는 FCP가 2.03초에서 1.60초로 줄고 점수도 96점에서 97점으로 올랐습니다. 하지만 실제 Chrome에서 첫 페인트는 2,122 ms에서 2,114 ms로 거의 변함이 없었으며, 실제 방문자의 대기 시간은 이전과 똑같았습니다.

4개 스타일시트에 fetchpriority low를 적용한 상품 페이지의 Lighthouse 리포트: 점수 97점, FCP 1.6초, Render-blocking requests는 여전히 빨간색이며 예상 절감 시간 800 ms

Lighthouse, 시뮬레이션 throttling

Lighthouse가 FCP로 보고한 시점에 모바일 Chrome에서 동일한 페이지의 상태: 여전히 흰 화면

모바일 Chrome, 동일 페이지

Lighthouse가 FCP로 제시한 시점에 Chrome은 여전히 흰 화면을 보여줍니다.

개선 작업이 효과가 있었는지 어떻게 확인할까요?

모바일에서 Lighthouse를 다시 실행해 보세요. 개선 후 점수는 96점에서 100점으로 올랐고 시뮬레이션 FCP는 2.0초에서 1.1초로 단축되었으며, 인사이트는 파일 12개의 빨간색에서 6.6 kB 스타일시트 단 하나가 남은 주황색으로 바뀌었습니다.

개선 전 모바일 Lighthouse 리포트: 점수 96점, FCP 2.0초, Render-blocking requests 빨간색 표시 및 예상 절감 시간 1,140 ms

개선 전

개선 후 모바일 Lighthouse 리포트: 점수 100점, FCP 1.1초, Render-blocking requests 주황색 표시 및 예상 절감 시간 없음

개선 후

Lighthouse 13.5.0, 모바일, 시뮬레이션 throttling.

세 번 실행한 뒤 중간값을 취하세요. 동일한 페이지에서도 실행마다 시뮬레이션 FCP가 최대 1.5초까지 차이 났습니다.

CoreDash는 이후 유입되는 방문부터 FCP와 LCP render delay를 곧바로 보여줍니다. 반면 CrUX와 Search Console은 최근 28일 데이터를 보여주기 때문에, 개선 작업 2주 뒤에도 표시되는 데이터의 절반은 이전 사이트의 기록입니다.

모바일 FCP나 LCP가 빨간색으로 표시되는 이유가 차단 CSS 때문인지 확인하고 싶으신가요? Core Web Vitals power hour를 통해 field data와 페이지의 trace를 함께 분석하고, 정리·병합하거나 defer해야 할 파일을 명확하게 짚어 드립니다.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

 

실시간 데이터. 28일 평균 말고.

CoreDash는 라우트, 디바이스, 브라우저, 회선별로 모든 지표를 쪼개서 보여줍니다.

CoreDash 둘러보기
Lighthouse Render-blocking requests: 해결 방법 Core Web Vitals Lighthouse Render-blocking requests: 해결 방법