SmashingMag 성능을 개선한 방법
게시 됨: 2022-03-10이 기사는 디자이너, 개발자 및 고객을 위한 웹 호스팅 솔루션의 전체 스펙트럼을 제공하는 Media Temple의 친애하는 친구의 도움을 받았습니다. 감사합니다, 친애하는 친구!
모든 웹 성능 이야기는 비슷하지 않습니까? 항상 오랫동안 기다려온 웹사이트 개편으로 시작됩니다. 완전히 세련되고 신중하게 최적화된 프로젝트가 시작되어 Lighthouse 및 WebPageTest에서 높은 순위를 기록하고 성능 점수를 뛰어 넘는 날입니다. 축하와 온 마음을 다한 성취감이 만연합니다. 리트윗, 댓글, 뉴스레터, Slack 스레드에 아름답게 반영되어 있습니다.
그러나 시간이 지남에 따라 흥분은 서서히 사라지고 긴급한 조정, 절실히 필요한 기능 및 새로운 비즈니스 요구 사항이 들어옵니다. 그리고 갑자기, 당신도 모르는 사이에 갑자기 코드 기반이 약간 과대해지고 파편화됩니다 . 스크립트는 조금 더 일찍 로드해야 하며 반짝이는 새로운 동적 콘텐츠는 제4자 스크립트와 초대받지 않은 게스트의 백도어를 통해 DOM으로 이동합니다.
우리는 Smashing에도 있었습니다. 많은 사람들이 알지는 못하지만 우리는 약 12명으로 구성된 아주 작은 팀입니다. 그들 중 많은 사람들이 파트타임으로 일하고 대부분은 보통 주어진 날에 다양한 모자를 쓰고 있습니다. 거의 10년 동안 성능이 우리의 목표였지만 실제로 전담 성능 팀이 없었습니다.
2017년 후반의 최신 재설계 이후, JavaScript 쪽(파트 타임)의 Ilya Pukhalski, CSS 쪽의 Michael Riethmueller(일주일에 몇 시간), 그리고 정말로 중요한 CSS로 마인드 게임을 하는 당신이 있었습니다. 몇 가지 너무 많은 것을 저글링하려고 합니다.

그렇게 우리는 바쁜 일상 속에서 성과를 잃어버렸습니다. 우리는 물건을 디자인하고 구축하고, 새로운 제품을 설정하고, 구성 요소를 리팩토링하고, 기사를 게시했습니다. 따라서 2020년 말까지 상황이 다소 통제 불능 상태가 되어 전체적으로 노란색을 띤 적색 Lighthouse 점수가 천천히 나타납니다. 우리는 그것을 고쳐야 했습니다.
그게 우리가 있었던 곳이야
여러분 중 일부는 우리가 JAMStack에서 실행되고 있다는 것을 알고 있을 것입니다. 모든 기사와 페이지는 Markdown 파일로 저장되고 Sass 파일은 CSS로 컴파일되며 JavaScript는 Webpack을 사용하여 청크로 분할되고 Hugo는 정적 페이지를 구축하여 Edge CDN에서 직접 제공합니다. 2017년에 우리는 전체 사이트를 Preact로 구축했지만 2019년에 React로 이전했으며 검색, 댓글, 인증 및 체크아웃을 위한 몇 가지 API와 함께 사용합니다.
전체 사이트는 점진적인 향상을 염두에 두고 구축되었습니다. 즉, 독자 여러분은 응용 프로그램을 부팅할 필요 없이 모든 Smashing 기사를 전체적으로 읽을 수 있습니다. 그리 놀라운 일도 아닙니다. 결국 게시된 기사는 수년에 걸쳐 크게 변경되지 않는 반면 멤버십 인증 및 체크아웃과 같은 동적 부분은 애플리케이션을 실행해야 합니다.
현재 약 2500개의 기사를 실시간으로 배포하기 위한 전체 빌드는 약 6분이 걸립니다. 중요한 CSS 삽입, Webpack의 코드 분할, 광고 및 기능 패널의 동적 삽입, RSS (재)생성 및 에지에서의 최종 A/B 테스트와 함께 빌드 프로세스 자체도 시간이 지남에 따라 상당히 짐승이 되었습니다.
2020년 초에 CSS 레이아웃 구성 요소의 대규모 리팩토링 을 시작했습니다. 우리는 CSS-in-JS나 styled-components를 사용하지 않았지만 대신 CSS로 컴파일될 Sass-modules의 좋은 구성 요소 기반 시스템을 사용했습니다. 2017년에 전체 레이아웃이 Flexbox로 구축되었고 2019년 중반에 CSS Grid 및 CSS Custom Properties로 다시 구축되었습니다. 그러나 일부 페이지는 새로운 광고 스팟과 새로운 제품 패널로 인해 특별 처리가 필요했습니다. 그래서 레이아웃이 작동하는 동안 잘 작동하지 않았고 유지 관리가 상당히 어려웠습니다.
또한 기본 탐색이 포함된 헤더는 동적으로 표시하려는 더 많은 항목을 수용하기 위해 변경해야 했습니다. 또한 사이트 전체에서 자주 사용되는 구성 요소를 리팩터링하고 거기에 사용된 CSS도 약간의 수정이 필요했습니다. 뉴스레터 상자가 가장 주목할만한 원인이었습니다. 우리는 유틸리티 우선 CSS로 일부 구성 요소를 리팩토링하는 것으로 시작했지만 전체 사이트에서 일관되게 사용되는 지점에 도달하지 못했습니다.
더 큰 문제는 매우 놀라운 일이 아니지만 수백 밀리초 동안 메인 스레드를 차단하는 대형 JavaScript 번들 이었습니다. 큰 JavaScript 번들은 단순히 기사를 게시하는 잡지에 적합하지 않은 것처럼 보일 수 있지만 실제로는 배후에서 많은 스크립팅이 발생하고 있습니다.
인증된 고객과 인증되지 않은 고객을 위한 다양한 상태의 구성 요소가 있습니다. 로그인하면 모든 제품을 최종 가격으로 표시하고 장바구니에 책을 추가할 때 현재 어떤 페이지에 있든 버튼을 탭하여 장바구니에 액세스할 수 있도록 하고 싶습니다. 광고는 방해가 되는 레이아웃 변경 을 일으키지 않고 신속하게 제공되어야 하며, 당사 제품을 강조하는 기본 제품 패널도 마찬가지입니다. 또한 독자가 이미 방문한 기사의 캐시된 버전과 함께 모든 정적 자산을 캐시하고 반복 보기를 위해 제공하는 서비스 작업자.
그래서 이 모든 스크립팅은 어느 시점에서 일어나야 했고, 스크립트가 꽤 늦게 도착했음에도 불구하고 읽기 경험을 소모했습니다. 솔직히 말해서, 우리는 성능에 주의를 기울이지 않고 사이트와 새로운 구성 요소에 공들여 작업하고 있었습니다(그리고 2020년에 염두에 두어야 할 몇 가지 다른 사항이 있었습니다). 전환점은 뜻밖에 찾아왔다. Harry Roberts는 (훌륭한) Web Performance Masterclass를 우리와 함께 온라인 워크샵으로 운영했으며 전체 워크샵에서 Smashing을 예로 들어 우리가 가진 문제를 강조하고 유용한 도구 및 지침과 함께 이러한 문제에 대한 솔루션을 제안했습니다.
워크숍 내내 나는 부지런히 메모를 하고 코드베이스를 재검토했다. 워크샵 당시 Lighthouse 점수는 홈페이지에서 60-68 , 기사 페이지에서 약 40-60 이었고 모바일에서는 분명히 더 나빴습니다. 워크샵이 끝나고 작업에 들어갔습니다.
병목 현상 식별
우리는 종종 우리가 얼마나 잘 수행하는지 이해하기 위해 특정 점수에 의존하는 경향이 있지만 너무 자주 단일 점수가 전체 그림을 제공하지 못합니다. David East가 자신의 기사에서 웅변적으로 언급했듯이 웹 성능은 단일 가치가 아닙니다. 배포입니다. 웹 경험이 강력하고 철저하게 최적화된 만능 성능이라고 해도 단순히 빠르기만 한 것은 아닙니다. 일부 방문자에게는 빠를 수 있지만 궁극적으로 다른 방문자에게는 더 느릴 수도 있습니다.
그 이유는 수없이 많지만 가장 중요한 것은 전 세계의 네트워크 조건과 장치 하드웨어의 엄청난 차이입니다. 종종 우리는 그러한 것들에 실제로 영향을 미칠 수 없기 때문에 대신 우리의 경험이 그것들을 수용하도록 해야 합니다.
본질적으로 우리의 임무는 빠른 경험의 비율을 늘리고 느린 경험의 비율을 줄이는 것입니다. 그러나 이를 위해서는 분포가 실제로 무엇인지에 대한 적절한 그림을 얻을 필요가 있습니다. 이제 분석 도구와 성능 모니터링 도구가 필요할 때 이 데이터를 제공할 것이지만 우리는 Chrome 사용자 경험 보고서인 CrUX를 구체적으로 살펴보았습니다. CrUX는 Chrome 사용자로부터 수집된 트래픽과 함께 시간 경과에 따른 성능 분포에 대한 개요를 생성합니다. 이 데이터의 대부분은 Google이 2020년에 발표한 Core Web Vitals와 관련되어 있으며 Lighthouse에 기여하고 노출됩니다.

우리는 전반적으로 8월과 9월경에 특히 하락하는 등 연중 내내 우리의 성과가 극적으로 퇴보했음을 알아차렸습니다. 이 차트를 보고 나면 실제로 일어난 일을 연구하기 위해 당시 라이브로 푸시했던 일부 PR을 다시 볼 수 있습니다.
이 즈음에 우리가 새로운 탐색 모음을 라이브로 출시했다는 사실을 깨닫는 데 시간이 걸리지 않았습니다. 모든 페이지에서 사용되는 탐색 모음은 탭하거나 클릭할 때 메뉴에 탐색 항목을 표시하기 위해 JavaScript에 의존했지만, 실제로는 JavaScript 비트가 app.js 번들에 번들로 포함되어 있었습니다. Time To Interactive를 개선하기 위해 번들에서 탐색 스크립트를 추출하여 인라인으로 제공하기로 결정했습니다.
거의 동시에 (오래된) 수동으로 생성된 중요한 CSS 파일에서 모든 템플릿(홈페이지, 기사, 제품 페이지, 이벤트, 구인 게시판 등)에 대해 중요한 CSS를 생성하는 자동화된 시스템으로 전환했으며, 빌드 시간. 그러나 우리는 자동으로 생성된 중요한 CSS가 얼마나 더 무거웠는지 실제로 깨닫지 못했습니다. 우리는 그것을 더 자세히 조사해야 했습니다.
또한 비슷한 시기에 웹 글꼴 로딩 을 조정하고 미리 로드와 같은 리소스 힌트를 사용하여 웹 글꼴을 보다 적극적으로 푸시하려고 했습니다. 그러나 웹 글꼴이 콘텐츠 렌더링을 지연시키고 전체 CSS 파일 다음으로 우선순위를 높게 지정했기 때문에 이는 우리의 성능 노력에 역행하는 것 같습니다.
이제 회귀의 일반적인 이유 중 하나는 JavaScript의 막대한 비용입니다. 따라서 JavaScript 종속성을 시각적으로 파악하기 위해 Webpack Bundle Analyzer와 Simon Hearne의 요청 맵도 살펴보았습니다. 처음에는 꽤 건강해 보였다.

CDN, 쿠키 동의 서비스 Cookiebot, Google Analytics, 제품 패널 및 맞춤 광고 제공을 위한 내부 서비스에 대한 몇 가지 요청이 있었습니다. 우리가 조금 더 자세히 볼 때까지는 많은 병목 현상이 있는 것처럼 보이지 않았습니다.
성능 작업에서 일부 중요한 페이지의 성능을 보는 것이 일반적입니다. 대부분은 홈페이지이고 대부분은 몇 개의 기사/제품 페이지입니다. 그러나 홈페이지는 하나뿐이지만 다양한 제품 페이지가 있을 수 있으므로 잠재 고객을 대표하는 페이지를 선택해야 합니다.
사실, 우리는 SmashingMag에 많은 코드와 디자인이 많은 기사를 게시하면서 수년 동안 무거운 GIF, 구문 강조 코드 조각, CodePen 삽입, 비디오/오디오가 포함된 말 그대로 수천 개의 기사를 축적했습니다. 포함 및 끝없는 주석의 중첩 스레드.
함께 모였을 때, 그들 중 다수는 과도한 메인 스레드 작업 과 함께 DOM 크기의 폭발적인 문제를 야기하여 수천 페이지의 경험을 느리게 했습니다. 광고가 있는 상태에서 일부 DOM 요소가 페이지 수명 주기 후반에 삽입되어 일련의 스타일 재계산 및 다시 그리기를 유발했으며, 이는 긴 작업을 생성할 수 있는 값비싼 작업이기도 합니다.
이 모든 것은 위의 차트에서 매우 가벼운 기사 페이지에 대해 생성한 지도에 표시되지 않았습니다. 그래서 우리는 가장 무거운 페이지(전능한 홈페이지, 가장 긴 페이지, 많은 비디오 포함 페이지, 많은 CodePen 포함 페이지)를 선택하고 최대한 최적화하기로 결정했습니다. 결과적으로 빠르면 단일 CodePen이 포함된 페이지도 더 빨라야 합니다.
이 페이지를 염두에 두고 지도가 약간 다르게 보였습니다. Vimeo 플레이어와 Vimeo CDN으로 향하는 거대한 굵은 선에 주목하세요. Smashing 기사에서 78개의 요청이 들어옵니다.

메인 스레드에 미치는 영향을 연구하기 위해 DevTools의 Performance 패널을 자세히 살펴보았습니다. 보다 구체적으로, 우리는 50ms 이상 지속되는 작업(오른쪽 상단 모서리에 빨간색 사각형으로 강조 표시됨)과 재계산 스타일(보라색 막대)이 포함된 작업을 찾고 있었습니다. 첫 번째는 값비싼 JavaScript 실행을 나타내는 반면, 후자는 DOM 및 최적이 아닌 CSS에 콘텐츠의 동적 삽입으로 인한 스타일 무효화를 노출합니다. 이것은 어디에서 시작해야 하는지에 대한 몇 가지 실행 가능한 지침을 제공했습니다. 예를 들어, 웹 글꼴 로드에 상당한 재페인트 비용이 발생하는 반면 JavaScript 청크는 여전히 메인 스레드를 차단할 만큼 충분히 무겁다는 것을 빠르게 발견했습니다.

기준으로 우리는 핵심 웹 바이탈을 매우 면밀히 살펴보고 모든 항목에서 좋은 점수를 얻을 수 있도록 노력했습니다. 우리는 특히 느린 3G, 400ms RTT 및 400kbps 전송 속도와 함께 느린 모바일 장치에 집중하기로 결정했습니다. Lighthouse가 가장 무거운 기사에 대해 완전한 빨간색 점수를 제공하고 사용하지 않는 JavaScript, CSS, 화면 외 이미지 및 크기에 대해 끊임없이 불평하면서 우리 사이트에 매우 만족하지 않은 것도 놀라운 일이 아닙니다.

데이터가 몇 개 있으면 중요한(그리고 중요하지 않은) CSS, JavaScript 번들, 긴 작업, 웹 글꼴 로드, 레이아웃 변경 및 타사에 중점을 두고 가장 무거운 세 가지 기사 페이지를 최적화하는 데 집중할 수 있습니다. -임베딩. 나중에 우리는 레거시 코드를 제거하고 새로운 최신 브라우저 기능을 사용하도록 코드베이스를 수정할 것입니다. 앞으로 많은 일이 있을 것 같았고 실제로 우리는 앞으로 몇 달 동안 매우 바빴습니다.
<head>
의 자산 순서 개선
아이러니하게도 우리가 가장 먼저 조사한 것은 위에서 확인한 모든 작업과 밀접하게 관련되어 있지도 않았습니다. 성능 워크샵에서 Harry는 각 페이지의 <head>
에 있는 자산의 순서를 설명하는 데 상당한 시간을 할애했습니다. 중요한 콘텐츠를 신속하게 제공한다는 것은 소스 코드에서 자산이 정렬되는 방식에 대해 매우 전략적이고 세심한 주의를 기울인다는 것을 의미합니다. .
이제 중요한 CSS가 웹 성능에 도움이 된다는 큰 폭로가 되어서는 안 됩니다. 그러나 리소스 힌트, 웹 글꼴 사전 로드, 동기 및 비동기 스크립트, 전체 CSS 및 메타데이터와 같은 다른 모든 자산의 순서가 얼마나 차이 가 나는지 조금 놀랐습니다.
우리는 모든 비동기 스크립트와 글꼴, 이미지 등과 같은 사전 로드된 모든 자산 앞에 중요한 CSS를 배치하여 전체 <head>
를 거꾸로 뒤집었습니다. 템플릿으로 사전 연결하거나 사전 로드할 자산을 분류했습니다. 특정 유형의 기사 및 페이지에 대해서만 중요한 이미지, 구문 강조 표시 및 비디오 삽입이 조기에 요청되도록 파일 유형을 지정합니다.
일반적으로 우리는 <head>
의 순서를 신중하게 조정하고 대역폭을 놓고 경쟁하는 사전 로드된 자산의 수를 줄이고 중요한 CSS를 올바르게 얻는 데 집중했습니다. <head>
순서에 대한 몇 가지 중요한 고려 사항에 대해 더 자세히 알아보려면 Harry가 CSS 및 네트워크 성능에 대한 기사에서 강조 표시합니다. 이 변경만으로도 전반적으로 Lighthouse 점수가 3-4점 정도였습니다.
자동화된 중요 CSS에서 수동 중요 CSS로 다시 이동
<head>
태그를 이동하는 것은 이야기의 간단한 부분이었습니다. 더 어려운 것은 중요한 CSS 파일의 생성과 관리였습니다. 2017년에 우리는 모든 화면 너비 에서 높이가 처음 1000픽셀 을 렌더링하는 데 필요한 모든 스타일을 수집하여 모든 템플릿에 대한 중요한 CSS를 수동으로 만들었습니다. 이것은 물론 중요 CSS 파일과 전체 CSS 파일 전체를 길들이기 위한 유지 관리 문제는 말할 것도 없고 성가시고 약간 고무적이지 않은 작업이었습니다.
그래서 우리는 이 프로세스를 빌드 루틴의 일부로 자동화하는 옵션을 조사했습니다. 실제로 사용할 수 있는 도구가 부족하지 않았기 때문에 몇 가지를 테스트하고 몇 가지 테스트를 실행하기로 결정했습니다. 우리는 그것들을 아주 빠르게 설정하고 실행할 수 있었습니다. 출력은 자동화된 프로세스에 충분히 좋은 것 같았으므로 몇 가지 구성을 조정한 후 연결하고 프로덕션으로 푸시했습니다. 이는 작년 7~8월경에 발생했으며 위의 CrUX 데이터의 급증 및 성능 저하에서 잘 시각화되었습니다. 특정 스타일을 추가하거나 다른 스타일을 제거하는 것과 같은 간단한 작업에서 종종 문제가 발생하여 구성을 왔다갔다했습니다. 예: 쿠키 스크립트가 초기화되지 않는 한 페이지에 실제로 포함되지 않는 쿠키 동의 프롬프트 스타일.
10월에 우리는 사이트에 몇 가지 주요 레이아웃 변경 사항을 도입했으며 중요한 CSS를 조사할 때 정확히 동일한 문제에 다시 부딪쳤습니다. 생성된 결과는 매우 장황했고 우리가 원하는 것이 아니었습니다. . 그래서 10월 말 실험으로 우리는 모두 우리의 강점을 모아 우리의 중요한 CSS 접근 방식을 다시 살펴보고 손으로 만든 중요한 CSS 가 얼마나 더 작은지 연구했습니다. 우리는 심호흡을 하고 주요 페이지의 코드 커버리지 도구에 대해 며칠을 보냈습니다. CSS 규칙을 수동으로 그룹화하고 중요한 CSS와 기본 CSS의 두 위치에서 중복 및 레거시 코드를 제거했습니다. 2017-2018년에 작성된 많은 스타일이 수년에 걸쳐 쓸모없게 되었기 때문에 실제로 매우 필요한 정리였습니다.
결과적으로 우리는 세 개의 중요한 수작업 CSS 파일과 현재 진행 중인 세 개의 추가 파일로 끝마쳤습니다.
- critical-homepage-manual.css(8.2KB, Brotlified)
- critical-article-manual.css(8KB, Brotlified)
- critical-articles-manual.css(6KB, Brotlified)
- critical-books-manual.css( 해야 할 작업 )
- critical-events-manual.css( 해야 할 작업 )
- critical-job-board-manual.css( 해야 할 작업 )
파일은 각 템플릿의 헤드에 인라인되며 현재 사이트에서 사용된(또는 실제로 더 이상 사용되지 않는) 모든 것을 포함하는 모놀리식 CSS 번들에 복제됩니다. 현재 우리는 전체 CSS 번들을 몇 개의 CSS 패키지로 분해하는 방법을 찾고 있으므로 잡지의 독자는 작업 게시판이나 책 페이지에서 스타일을 다운로드하지 않지만 해당 페이지에 도달하면 빠른 렌더링을 얻을 수 있습니다. 중요한 CSS를 사용하여 해당 페이지의 나머지 CSS를 비동기식으로 가져옵니다. 해당 페이지에서만 가능합니다.
확실히, 손으로 만든 중요한 CSS 파일은 크기가 훨씬 작지 않았습니다. 중요한 CSS 파일의 크기를 약 14% 줄 였습니다. 그러나 중복 및 재정의 스타일 없이 위에서 끝까지 올바른 순서로 필요한 모든 것을 포함했습니다. 이것은 올바른 방향으로 나아가는 것 같았고 Lighthouse에서 3-4점을 더 늘렸습니다. 우리는 진전을 이루고 있었습니다.
웹 글꼴 로드 변경
손끝에서 글꼴 font-display
를 사용하면 글꼴 로드가 과거에는 문제였던 것 같습니다. 불행히도 우리의 경우에는 옳지 않습니다. 독자 여러분, Smashing Magazine의 여러 기사를 방문하는 것 같습니다. 당신은 또한 또 다른 기사를 읽기 위해 종종 사이트를 다시 방문합니다. 아마도 몇 시간이나 며칠 후, 또는 일주일 후에. 사이트 전체에서 사용되는 font-display
와 관련된 문제 중 하나는 기사 사이를 많이 이동하는 독자의 경우 대체 글꼴과 웹 글꼴 사이에 플래시가 많이 발생한다는 점이었습니다. 제대로 캐시됨).
그것은 괜찮은 사용자 경험처럼 느껴지지 않았기 때문에 우리는 옵션을 조사했습니다. Smashing에서는 두 가지 주요 서체 를 사용합니다. 제목에는 Mija, 본문에는 Elena를 사용합니다. Mija는 두 가지 두께(일반 및 볼드)로 제공되고 Elena는 세 가지 두께(일반, 기울임꼴, 볼드)로 제공됩니다. 몇 년 전 Elena의 Bold Italic을 재설계하는 동안 단 몇 페이지에서만 사용했기 때문에 삭제했습니다. 사용하지 않는 문자와 유니코드 범위를 제거하여 다른 글꼴의 하위 집합을 지정합니다.
우리 기사는 대부분 텍스트로 설정되어 있으므로 사이트에서 가장 큰 콘텐츠가 포함된 페인트는 기사의 텍스트 첫 단락이나 작성자의 사진 중 대부분이라는 것을 발견했습니다. 즉, 최소한의 리플로우로 웹 글꼴로 우아하게 변경하면서 첫 번째 단락이 대체 글꼴로 빠르게 표시되도록 특별히 주의해야 합니다.
첫 페이지의 초기 로딩 경험을 자세히 살펴보십시오(3배 느려짐):
솔루션을 찾을 때 네 가지 주요 목표가 있었습니다.
- 첫 번째 방문에서 텍스트를 대체 글꼴로 즉시 렌더링합니다.
- 레이아웃 변경을 최소화하기 위해 대체 글꼴 및 웹 글꼴의 글꼴 메트릭을 일치시킵니다.
- 모든 웹 글꼴을 비동기식으로 로드하고 한 번에 모두 적용합니다(최대 1리플로우).
- 후속 방문에서 모든 텍스트를 웹 글꼴로 직접 렌더링합니다(플래시 또는 리플로우 없이).
처음에는 실제로 font-display: swap on font-face
를 사용하려고 했습니다. 이것은 가장 간단한 옵션인 것 같았지만 위에서 언급한 것처럼 일부 독자는 여러 페이지를 방문하므로 사이트 전체에서 렌더링하고 있던 6개의 글꼴로 인해 깜박임이 많이 발생했습니다. 또한 글꼴 표시 만으로는 요청을 그룹화하거나 다시 칠할 수 없습니다.
또 다른 아이디어는 초기 방문 시 모든 글꼴을 대체 글꼴 로 렌더링한 다음 모든 글꼴을 비동기적으로 요청 및 캐시하고 후속 방문에서만 캐시에서 직접 웹 글꼴을 제공하는 것입니다. 이 접근 방식의 문제는 많은 독자가 검색 엔진에서 오고 있고 그들 중 적어도 일부는 해당 페이지를 볼 수 있다는 것입니다. 그리고 우리는 시스템 글꼴로만 기사를 렌더링하고 싶지 않았습니다.
그럼 뭐죠?
2017년부터 우리는 기본적으로 두 가지 렌더링 단계를 설명하는 웹 글꼴 로드에 대해 2단계 렌더링 접근 방식을 사용하고 있습니다. 과거에 우리는 사이트에서 가장 자주 사용되는 가중치인 Mija Bold 및 Elena Regular의 최소한의 하위 집합을 만들었습니다. 두 하위 집합에는 라틴 문자, 구두점, 숫자 및 몇 가지 특수 문자만 포함됩니다. 이러한 글꼴( ElenaInitial.woff2 및 MijaInitial.woff2 )은 크기가 매우 작았습니다. 종종 크기가 약 10–15KB에 불과했습니다. 글꼴 렌더링의 첫 번째 단계에서 이 두 글꼴로 전체 페이지를 표시합니다.

성공적으로 로드된 글꼴과 아직 로드되지 않은 글꼴에 대한 정보를 제공하는 글꼴 로드 API를 사용하여 이를 수행합니다. 무대 뒤에서 .wf-loaded-stage1 클래스를 body 에 추가하고 스타일이 해당 글꼴의 콘텐츠를 렌더링함으로써 발생합니다.
.wf-loaded-stage1 article, .wf-loaded-stage1 promo-box, .wf-loaded-stage1 comments { font-family: ElenaInitial,sans-serif; } .wf-loaded-stage1 h1, .wf-loaded-stage1 h2, .wf-loaded-stage1 .btn { font-family: MijaInitial,sans-serif; }
글꼴 파일은 매우 작기 때문에 네트워크를 매우 빠르게 통과할 수 있기를 바랍니다. 그런 다음 독자가 실제로 기사 읽기를 시작할 수 있으므로 글꼴의 전체 가중치를 비동기적으로 로드하고 본문 에 .wf-loaded-stage2 를 추가합니다.
.wf-loaded-stage2 article, .wf-loaded-stage2 promo-box, .wf-loaded-stage2 comments { font-family: Elena,sans-serif; } .wf-loaded-stage2 h1, .wf-loaded-stage2 h2, .wf-loaded-stage2 .btn { font-family: Mija,sans-serif; }
따라서 페이지를 로드할 때 독자는 먼저 작은 하위 집합 웹 글꼴을 빠르게 얻은 다음 전체 글꼴 모음으로 전환합니다. 이제 기본적으로 대체 글꼴과 웹 글꼴 간의 이러한 전환은 네트워크를 통해 먼저 오는 항목에 따라 무작위로 발생합니다. 기사를 읽기 시작했을 때 상당히 혼란스럽게 느껴질 수 있습니다. 따라서 글꼴을 전환할 시기를 결정하도록 브라우저에 맡기는 대신, 리플로우 영향을 최소로 줄이는 repaints를 그룹화 합니다.
/* Loading web fonts with Font Loading API to avoid multiple repaints. With help by Irina Lipovaya. */ /* Credit to initial work by Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // If the Font Loading API is supported... // (If not, we stick to fallback fonts) if ("fonts" in document) { // Create new FontFace objects, one for each font let ElenaRegular = new FontFace( "Elena", "url(/fonts/ElenaWebRegular/ElenaWebRegular.woff2) format('woff2')" ); let ElenaBold = new FontFace( "Elena", "url(/fonts/ElenaWebBold/ElenaWebBold.woff2) format('woff2')", { weight: "700" } ); let ElenaItalic = new FontFace( "Elena", "url(/fonts/ElenaWebRegularItalic/ElenaWebRegularItalic.woff2) format('woff2')", { style: "italic" } ); let MijaBold = new FontFace( "Mija", "url(/fonts/MijaBold/Mija_Bold-webfont.woff2) format('woff2')", { weight: "700" } ); // Load all the fonts but render them at once // if they have successfully loaded let loadedFonts = Promise.all([ ElenaRegular.load(), ElenaBold.load(), ElenaItalic.load(), MijaBold.load() ]).then(result => { result.forEach(font => document.fonts.add(font)); document.documentElement.classList.add('wf-loaded-stage2'); // Used for repeat views sessionStorage.foutFontsStage2Loaded = true; }).catch(error => { throw new Error(`Error caught: ${error}`); }); }
그러나 첫 번째 작은 글꼴 하위 집합이 네트워크를 통해 빠르게 전달되지 않으면 어떻게 될까요? 우리는 이것이 우리가 원하는 것보다 더 자주 일어나는 것 같다는 것을 알아차렸습니다. 이 경우 3초의 제한 시간이 만료되면 최신 브라우저는 시스템 글꼴(글꼴 스택에서는 Arial이 됨)로 폴백한 다음 나중에 각각 전체 Elena 또는 Mija로 전환하기 위해 ElenaInitial 또는 MijaInitial 로 전환합니다. . 그것은 우리 시음에 너무 많은 깜박임을 만들어 냈습니다. 우리는 처음에 (Network Information API를 통해) 느린 네트워크에 대해서만 첫 번째 단계 렌더링을 제거하는 것에 대해 생각했지만 완전히 제거하기로 결정했습니다.

그래서 10월에 중간 단계와 함께 하위 집합을 모두 제거했습니다. Elena 및 Mija 글꼴의 모든 가중치가 클라이언트에 의해 성공적으로 다운로드되어 적용할 준비가 될 때마다 2단계를 시작하고 모든 것을 한 번에 다시 칠합니다. 그리고 리플로우가 눈에 띄지 않게 하기 위해 대체 글꼴과 웹 글꼴을 일치시키는 데 약간의 시간을 보냈습니다. 이는 대부분 페이지의 첫 번째 보이는 부분에 그려진 요소에 약간 다른 글꼴 크기와 줄 높이를 적용하는 것을 의미했습니다.
이를 위해 font-style-matcher
와 (에헴, 에헴) 몇 가지 매직 넘버를 사용했습니다. 이것이 우리가 처음에 -apple-system 및 Arial을 전역 대체 글꼴로 사용했던 이유이기도 합니다. San Francisco( -apple-system 을 통해 렌더링됨)는 Arial보다 약간 더 나은 것처럼 보이지만 사용할 수 없는 경우 대부분의 OS에 널리 퍼져 있다는 이유로 Arial을 사용하기로 결정했습니다.
CSS에서는 다음과 같이 보일 것입니다.
.article__summary { font-family: -apple-system,Arial,BlinkMacSystemFont,Roboto Slab,Droid Serif,Segoe UI,Ubuntu,Cantarell,Georgia,sans-serif; font-style: italic; /* Warning: magic numbers ahead! */ /* San Francisco Italic and Arial Italic have larger x-height, compared to Elena */ font-size: 0.9213em; line-height: 1.487em; } .wf-loaded-stage2 .article__summary { font-family: Elena,sans-serif; font-size: 1em; /* Original font-size for Elena Italic */ line-height: 1.55em; /* Original line-height for Elena Italic */ }
이것은 꽤 잘 작동했습니다. 우리는 즉시 텍스트를 표시하고 웹 글꼴은 그룹화된 화면에 나타나며 이상적으로는 첫 번째 보기에서 정확히 한 번의 리플로우가 발생하고 후속 보기에서는 전혀 리플로우가 발생하지 않습니다.
글꼴이 다운로드되면 서비스 워커의 캐시에 저장합니다. 후속 방문에서 우리는 먼저 글꼴이 이미 캐시에 있는지 확인합니다. 그렇다면 서비스 워커의 캐시에서 검색하여 즉시 적용합니다. 그렇지 않은 경우 fallback-web-font-switcheroo 로 다시 시작합니다.
이 솔루션은 상대적으로 빠른 연결에서 리플로우 수를 최소(하나)로 줄이면서 동시에 글꼴을 캐시에 지속적이고 안정적으로 유지합니다. 앞으로 우리는 매직 넘버를 f-mod로 대체하기를 진심으로 희망합니다. 아마도 Zach Leatherman은 자랑스러워 할 것입니다.
모놀리식 JS 식별 및 분해
DevTools의 성능 패널에서 메인 스레드를 연구했을 때 우리는 무엇을 해야 하는지 정확히 알고 있었습니다. 70ms에서 580ms 사이에 걸리는 8개의 긴 작업이 있어 인터페이스를 차단하고 응답하지 않게 되었습니다. 일반적으로 비용이 가장 많이 드는 스크립트는 다음과 같습니다.
- uc.js , 쿠키 프롬프트 스크립팅(70ms)
- 들어오는 full.css 파일(176ms)로 인한 스타일 재계산(중요 CSS는 모든 뷰포트에서 1000px 높이 미만의 스타일을 포함하지 않음)
- 패널, 장바구니 등을 관리하기 위해 로드 이벤트에서 실행되는 광고 스크립트 + 스타일 재계산(276ms)
- 웹 글꼴 전환, 스타일 재계산(290ms)
- app.js 평가(580ms)
우리는 가장 해로운 것, 말하자면 가장 긴 작업에 초점을 맞췄습니다.

첫 번째는 글꼴 변경(폴백 글꼴에서 웹 글꼴로)로 인한 값비싼 레이아웃 재계산으로 인해 발생했으며, 이로 인해 290ms 이상의 추가 작업이 발생했습니다(빠른 랩톱 및 빠른 연결에서). 글꼴 로딩에서 1단계를 제거함으로써 우리는 약 80ms를 얻을 수 있었습니다. 50ms 예산을 훨씬 초과했기 때문에 충분하지 않았습니다. 그래서 우리는 더 깊이 파고들기 시작했습니다.
재계산이 발생한 주된 이유는 대체 글꼴과 웹 글꼴 간의 엄청난 차이 때문이었습니다. 대체 글꼴 및 웹 글꼴의 줄 높이와 크기를 일치 시킴으로써 텍스트 줄이 대체 글꼴의 새 줄에서 줄바꿈되지만 이전 줄에 약간 작아지고 맞춰지는 많은 상황을 피할 수 있었습니다. 전체 페이지의 지오메트리를 크게 변경하고 결과적으로 레이아웃을 크게 변경합니다. 우리는 letter-spacing
과 word-spacing
간격도 가지고 놀았지만 좋은 결과를 내지 못했습니다.
이러한 변경으로 인해 50-80ms를 더 줄일 수 있었지만 콘텐츠를 대체 글꼴로 표시하고 나중에 웹 글꼴로 콘텐츠를 표시하지 않고는 120ms 미만으로 줄일 수 없었습니다. 분명히, 글꼴 전환으로 인한 비용이 많이 드는 리플로우 없이 서비스 작업자의 캐시에서 직접 검색된 글꼴로 결과 페이지 보기가 렌더링되므로 처음 방문자에게만 큰 영향을 미칠 것입니다.
그건 그렇고, 우리의 경우 대부분의 긴 작업이 대규모 JavaScript에 의해 발생하는 것이 아니라 레이아웃 재계산 및 CSS의 구문 분석에 의해 발생한다는 사실을 알아차리는 것이 매우 중요합니다. cleaning, especially watching out for situations when styles are overwritten. In some way, it was good news because we didn't have to deal with complex JavaScript issues that much. However, it turned out not to be straightforward as we are still cleaning up the CSS this very day. We were able to remove two Long Tasks for good, but we still have a few outstanding ones and quite a way to go. Fortunately, most of the time we aren't way above the magical 50ms threshold.
The much bigger issue was the JavaScript bundle we were serving, occupying the main thread for a whopping 580ms. Most of this time was spent in booting up app.js which contains React, Redux, Lodash, and a Webpack module loader. The only way to improve performance with this massive beast was to break it down into smaller pieces. So we looked into doing just that.
With Webpack, we've split up the monolithic bundle into smaller chunks with code-splitting , about 30Kb per chunk. We did some package.json cleansing and version upgrade for all production dependencies, adjusted the browserlistrc setup to address the two latest browser versions, upgraded to Webpack and Babel to the latest versions, moved to Terser for minification, and used ES2017 (+ browserlistrc) as a target for script compilation.
We also used BabelEsmPlugin to generate modern versions of existing dependencies. Finally, we've added prefetch links to the header for all necessary script chunks and refactored the service worker, migrating to Workbox with Webpack (workbox-webpack-plugin).

Remember when we switched to the new navigation back in mid-2020, just to see a huge performance penalty as a result? The reason for it was quite simple. While in the past the navigation was just static plain HTML and a bit of CSS, with the new navigation, we needed a bit of JavaScript to act on opening and closing of the menu on mobile and on desktop. That was causing rage clicks when you would click on the navigation menu and nothing would happen, and of course, had a penalty cost in Time-To-Interactive scores in Lighthouse.
We removed the script from the bundle and extracted it as a separate script . Additionally, we did the same thing for other standalone scripts that were used rarely — for syntax highlighting, tables, video embeds and code embeds — and removed them from the main bundle; instead, we granularly load them only when needed.

However, what we didn't notice for months was that although we removed the navigation script from the bundle, it was loading after the entire app.js bundle was evaluated, which wasn't really helping Time-To-Interactive (see image above). We fixed it by preloading nav.js and deferring it to execute in the order of appearance in the DOM, and managed to save another 100ms with that operation alone. By the end, with everything in place we were able to bring the task to around 220ms.

We managed to get some improvement in place, but still have quite a way to go, with further React and Webpack optimizations on our to-do list. At the moment we still have three major Long Tasks — font switch (120ms), app.js execution (220ms) and style recalculations due to the size of full CSS (140ms). For us, it means cleaning up and breaking up the monolithic CSS next.
It's worth mentioning that these results are really the best-scenario- results. On a given article page we might have a large number of code embeds and video embeds, along with other third-party scripts and customer's browser extensions that would require a separate conversation.
Dealing With 3rd-Parties
Fortunately, our third-party scripts footprint (and the impact of their friends' fourth-party-scripts) wasn't huge from the start. But when these third-party scripts accumulated, they would drive performance down significantly. This goes especially for video embedding scripts , but also syntax highlighting, advertising scripts, promo panels scripts and any external iframe embeds.
Obviously, we defer all of these scripts to start loading after the DOMContentLoaded event, but once they finally come on stage, they cause quite a bit of work on the main thread. This shows up especially on article pages, which are obviously the vast majority of content on the site.
The first thing we did was allocating proper space to all assets that are being injected into the DOM after the initial page render. It meant width
and height
for all advertising images and the styling of code snippets. We found out that because all the scripts were deferred, new styles were invalidating existing styles, causing massive layout shifts for every code snippet that was displayed. We fixed that by adding the necessary styles to the critical CSS on the article pages.
We've re-established a strategy for optimizing images (preferably AVIF or WebP — still work in progress though). All images below the 1000px height threshold are natively lazy-loaded (with <img loading=lazy>
), while the ones on the top are prioritized ( <img loading=eager>
). The same goes for all third-party embeds.
We replaced some dynamic parts with their static counterparts — eg while a note about an article saved for offline reading was appearing dynamically after the article was added to the service worker's cache, now it appears statically as we are, well, a bit optimistic and expect it to be happening in all modern browsers.
As of the moment of writing, we're preparing facades for code embeds and video embeds as well. Plus, all images that are offscreen will get decoding=async
attribute, so the browser has a free reign over when and how it loads images offscreen, asynchronously and in parallel.

To ensure that our images always include width and height attributes, we've also modified Harry Roberts' snippet and Tim Kadlec's diagnostics CSS to highlight whenever an image isn't served properly. It's used in development and editing but obviously not in production.
One technique that we used frequently to track what exactly is happening as the page is being loaded, was slow-motion loading .
First, we've added a simple line of code to the diagnostics CSS, which provides a noticeable outline for all elements on the page.
* { outline: 3px solid red }
* { outline: 3px solid red }

* { outline: 3px red }
and observing the boxes as the browser is rendering the page. (큰 미리보기)Then we record a video of the page loaded on a slow and fast connection. Then we rewatch the video by slowing down the playback and moving back and forward to identify where massive layout shifts happen.
Here's the recording of a page being loaded on a fast connection:
And here's the recording of a recording being played to study what happens with the layout:
By auditing the layout shifts this way, we were able to quickly notice what's not quite right on the page, and where massive recalculation costs are happening. As you probably have noticed, adjusting the line-height
and font-size
on headings might go a long way to avoid large shifts.
With these simple changes alone, we were able to boost performance score by a whopping 25 Lighthouse points for the video-heaviest article, and gain a few points for code embeds.
Enhancing The Experience
We've tried to be quite strategic in pretty much everything from loading web fonts to serving critical CSS. However, we've done our best to use some of the new technologies that have become available last year.
We are planning on using AVIF by default to serve images on SmashingMag, but we aren't quite there yet, as many of our images are served from Cloudinary (which already has beta support for AVIF), but many are directly from our CDN yet we don't really have a logic in place just yet to generate AVIFs on the fly. That would need to be a manual process for now.
We're lazy rendering some of the offset components of the page with content-visibility: auto . For example, the footer, the comments section, as well as the panels way below the first 1000px height threshold, are all rendered later after the visible portion of each page has been rendered.
우리는 link rel="prefetch"
및 link rel="prerender"
(NoPush 프리페치)를 사용하여 추가 탐색에 사용될 가능성이 매우 높은 페이지의 일부 부분(예: 첫 번째 자산을 미리 가져오기 위해)을 사용했습니다. 첫 페이지의 기사(아직 논의 중).
또한 작성자 이미지를 미리 로드 하여 콘텐츠가 포함된 가장 큰 페인트와 모든 작성자 이미지에 사용되는 춤추는 고양이 이미지(탐색용) 및 그림자와 같이 각 페이지에서 사용되는 일부 주요 자산을 미리 로드합니다. 그러나 더 큰 화면(>800px)에 있는 경우에만 모든 항목이 사전 로드되지만 더 정확한 대신 Network Information API를 사용하는 방법을 찾고 있습니다.
또한 레거시 코드를 제거하고 여러 구성 요소를 리팩토링하고 텍스트 장식 건너뛰기 조합으로 완벽한 밑줄을 만들기 위해 사용했던 텍스트 그림자 트릭을 제거하여 전체 CSS 및 모든 중요한 CSS 파일의 크기를 줄였습니다. - 잉크 및 텍스트 장식 두께 (마침내!).
해야 할 일
우리는 사이트의 모든 사소한 변경 사항과 주요 변경 사항을 해결하는 데 상당한 시간을 할애했습니다. 데스크톱에서는 상당히 개선되었으며 모바일에서는 눈에 띄게 향상되었습니다. 글을 쓰는 시점에서 우리 기사는 데스크톱에서 평균 90~100점, 모바일에서 약 65 ~80점을 기록하고 있습니다.


모바일에서 낮은 점수를 받은 이유는 분명히 앱의 부팅과 전체 CSS 파일의 크기로 인해 인터랙티브한 시간과 Total Blocking 시간이 좋지 않기 때문입니다. 그래서 거기에는 아직 해야 할 일이 있습니다.
다음 단계에서 우리는 현재 CSS의 크기를 더 줄이는 방법 을 찾고 있으며, 특히 JavaScript와 유사하게 모듈로 세분화하여 CSS의 일부(예: 체크아웃 또는 작업 게시판 또는 책/전자책)를 로드하는 경우에만 필요.
우리는 또한 현재로서는 사소하지 않은 것처럼 보이지만 app.js 의 성능 영향을 줄이기 위해 모바일에서 실험을 추가로 묶는 옵션을 탐색합니다. 마지막으로 쿠키 프롬프트 솔루션에 대한 대안을 살펴보고 CSS clamp()
로 컨테이너를 재구축하고, 패딩-하단 비율 기술을 aspect-ratio
대체하고, AVIF에서 가능한 한 많은 이미지를 제공하는 방법을 조사할 것입니다.
그게 다야, 여러분!
바라건대, 이 작은 사례 연구가 귀하에게 유용할 것이며 아마도 프로젝트에 즉시 적용할 수 있는 한두 가지 기술이 있을 것입니다. 결국 성능은 모든 미세한 세부 사항의 총합에 관한 것입니다. 이 모든 것이 추가될 때 고객의 경험을 만들거나 깨뜨릴 수 있습니다.
우리는 성능 향상을 위해 최선을 다하고 있지만 사이트의 접근성 과 콘텐츠를 개선하기 위해 노력하고 있습니다. 따라서 옳지 않은 점이나 Smashing Magazine을 더욱 개선하기 위해 할 수 있는 일을 발견하면 이 기사에 대한 의견을 통해 알려주십시오.
마지막으로, 이와 같은 기사에 대한 최신 정보를 계속 받고 싶으시다면 친절한 웹 팁, 유용한 정보, 도구 및 기사, Smashing 고양이의 계절별 셀렉션을 제공하는 이메일 뉴스레터를 구독하십시오.