JavaScript 지연 로드 및 초기 페이지 로드 시 임시 콘텐츠 표시

이렇게 하는 것은 어떨까요: Discourse의 자바스크립트 지연 로드(defer)

가능한 경우 모든 자바스크립트에 defer 속성을 추가하세요. 자바스크립트 로딩과 실행을 지연시키면 브라우저가 HTML 파싱, 렌더링 및 페인팅을 시작할 수 있습니다.

이로 인해 Discourse 부팅 과정의 초기 단계(또는 그 이전)에 정적 중간 콘텐츠가 꽤 일찍 표시될 수 있습니다. 이는 첫 페이지 로드 시 사용자가 인지하는 페이지 로딩 속도를 빠르게 하는 데 도움이 될 것입니다.

정적 중간 콘텐츠 아이디어:

  • 로고와 로딩 스피너가 있는 스플래시 화면
  • 백엔드에서 가져온 게시물로 구성된 토픽 뷰

POC 및 PR

최신 개념 증명(POC) 및 PR은 이 게시물을 참고하세요.


현재 vendor-javascript 및 그 이전의 모든 자바스크립트는 지연되지(defer되지) 않습니다.
@see: https://github.com/rr-it/discourse/commit/328efd5c055f5f2a4d93b5e52268cfe92913faf7

이를 해결하는 방법에 대한 아이디어는 매우 환영합니다.


자바스크립트 async vs. defer vs. 없음

defer를 포함한 자바스크립트 로딩 옵션에 대한 자세한 정보: Efficiently load JavaScript with defer and async
(이는 실제 Discourse 부팅 속도를 높이는 것에 관한 것이 아닙니다.)


Fastboot/리하이드레이션(Rehydration)

이 기사를 읽었습니다:
거기서의 결론은 Fastboot/리하이드레이션의 구현인 것으로 보입니다.
이 작업에 대한 타임라인이 있나요?

이는 EmberJS가 부팅되고 다시 렌더링된 후에야 LCP가 발생하는 결과를 초래하며, Google의 새로운 랭킹 기준에 관한 주요 문제를 해결하지 못합니다.

이는 Discourse에서 LCP를 해결하기 위한 우리의 현재 중기 계획입니다.

다행히도 Chrome 88부터는 더 이상 사실이 아닙니다! :rocket:
나도 이제서야 이 사실을 알게 되었습니다. :))

“이 변경 사항 이전에는 요소가 제거되면 더 이상 유효한 LCP 후보로 고려되지 않았습니다. […] 이 변경 사항 이후에는 요소가 제거되어도 여전히 유효한 LCP 후보로 간주됩니다.”

“DOM에서 나중에 제거되는 콘텐츠를 최대 콘텐츠 렌더링(Largest Contentful Paint)의 가능한 대상으로 포함하는 이 변경 사항은, 동일한 크기의 이미지 [Discourse의 경우: 텍스트 요소]가 여러 번 삽입되는 사이트의 LCP 시간을 개선합니다. 이는 캐러셀(carousel)에서 흔히 볼 수 있는 패턴이며, 서버 사이드 렌더링을 수행하는 일부 JavaScript 프레임워크에서도 볼 수 있습니다.”


LCP 변경 이력

미래에 더 많은 좋은 변경 사항이 있을 수 있습니다:

POC가 구현된 주제 페이지의 시뮬레이션 통계 정보를 몇 가지 소개합니다.

Lighthouse: “값은 추정치이며 달라질 수 있습니다.”

WebPageTest

Moto4G 시뮬레이션


참고: 우리는 맨 위의 검은 화살표입니다.


참고:

  • 0초~2초 - 빈 화면:
    WebPageTest는 JavaScript의 defer를 무시하고 첫 페인트를 수행하기 전에 모든 JavaScript를 다운로드합니다 - 이는 실제 기기에서는 올바르게 작동합니다.
  • 2.5초 - LCP: 서버 렌더링에서 제공되는 정적 콘텐츠
  • 3.5초 - 시각적 변경: 로고 로드 완료
  • 6.5초 - 시각적 변경: EmberJs 렌더링에서 제공되는 콘텐츠
  • 7초 - 시각적 완성

PageSpeed Insights

Largest Contentful Paint 요소

PageSpeed는 서버 렌더링에서 제공되는 정적 텍스트 노드를 FCP LCP 요소로 올바르게 식별합니다:
div.row > div.topic-body > div.post > p

EmberJs 렌더링 텍스트 노드:
div.row > div.topic-body > div.regular.contents > div.cooked > p

그러나 PageSpeed가 시뮬레이션된 결과에 대해 올바르게 식별된 정적 텍스트 노드를 사용하지 않는 것으로 보입니다: 시뮬레이션된 FCP와 LCP가 너무 큽니다.

실제 사용자 데이터

POC가 구현된 Chrome UX Report에서 “실제” 데이터를 얻기 위해 14~28일을 더 기다려 보겠습니다.

테스트된 주제 페이지에 POC가 구현되지 않은 경우의 통계:
(해당 데이터는 전체 오리진이 아닌 이 단일 주제 URL에 대한 것입니다.)

오, 정말 흥미로운 발견이네요! 잘하셨습니다!

이 확장 프로그램 https://chrome.google.com/webstore/detail/web-vitals/ahfhijdlegdabablpippeagghigmibma? 에서 어떤 결과를 얻으시나요?

Web Vitals Chrome 확장 프로그램을 통해 측정

  • 데스크톱
  • Chromium 버전 90.0.4430.212
  • 새 시크릿 창에서 첫 번째 로드


First Input Delay에 대한 참고: 페이지가 완전히 로드될 때까지 기다린 후 배경을 클릭했습니다. 즉, EmberJs 렌더링이 완료된 후입니다.


First Input Delay에 대한 참고: 여기서는 정적 콘텐츠가 처음 보일 때 즉시 배경을 클릭했습니다. 이 FID 값에 제 반응 시간 :sloth: 을 더해야 합니다.

그래프 막대 아래의 *백분위수(percentiles)*에 대한 추가 참고:
백분위수는 측정된 값을 원본 값과 비교할 뿐이므로 그다지 관련이 없습니다. 원본은 Discourse의 서브폴더 설치된 TYPO3 웹페이지입니다.

훌륭한 아이디어네요! @rrit

전적으로 동의합니다. Discourse는 JS가 무거운 웹 앱이라 매우 느립니다. CSS/JS 파일을 지연 로드할 수 있다면 LCP, FCP, FID, CLS 개선에 큰 도움이 될 것입니다.

이 기능이 실제로 적용되는 모습을 보고 싶습니다. 저를 비롯해 많은 분들이 이 문제를 겪고 있습니다. 모든 Discourse 사이트가 Core Web Vitals 기준을 충족하지 못하고 있습니다. 사용자에게 첫 방문 시 빠른 STATIC HTML 페이지를 제공하고, 또는 초기 로드 시 모든 JS/CSS 로직을 지연 처리한다면 모든 페이지의 속도를 높이고 CWV 점수를 통과시킬 수 있습니다. Core Discourse 업데이트에 이 기능이 반영되는 것을 기대하고 있습니다.

Core Web Vitals 기준을 충족하지 못해 모든 Discourse 사이트의 Google 랭킹이 하락하고 있습니다.

코어에서 이 기능을 실험해 볼 의향이 있습니다. 스타일이 다른 콘텐츠가 번쩍이는 현상은 약간 어색할 수 있으므로, 처음에는 기본적으로 비활성화된 “실험적” 사이트 설정 뒤에 두는 방식으로 시작하겠습니다. 이렇게 하면 사이트 관리자가 원할 경우 활성화할 수 있습니다.

@rrit님, PR에 사이트 설정을 추가해 볼 수 있으신가요? 설정이 활성화/비활성화되었을 때 동작을 검증하기 위해 RSpec 테스트를 추가하는 것도 좋습니다.

그렇다면 서버가 렌더링한 페이지에 전체 화면 스피너(너비 100%, 높이 100%)를 배치하고, Ember 앱이 최종적으로 부팅되면 이를 교체하는 방식으로 극도로 낮은 LCP를 얻을 수 있다는 뜻인가요?

이 스피너를 Discourse UI를 모방한 SVG로 만들어 전환이 더 부드럽고 FOUC(First Unstyled Content) 현상이 덜 나타나도록 할 수 있습니다.

핵심은 LCP "후보(candidate)"라는 점인 것 같습니다.

실제로 가장 큰 요소(최소 동일한 크기)로 최종 렌더링될 때만 최대 콘텐츠 페인트(LCP)로 간주되지 않을까요?

그래서 크롤러 뷰를 사용하는 것이 꽤 잘 작동하는 것 같습니다. 콘텐츠(즉, 텍스트)가 대부분 동일하기 때문이죠?

(위 스크린샷을 바탕으로 추측하는 것일 뿐, 전체 화면 스피너는 직접 테스트해 보지 않았습니다.)

기능 플래그는 구현되었습니다.

저는 Ruby 개발자가 전혀 아닙니다 - 이 부분에서는 확실히 도움이 필요합니다.

main에 PR을 만들기 전에 POC를 discourse/discourse 저장소의 새 브랜치로 푸시하는 것이 좋을까요?

이 기능에 대한 제 PR입니다:

@david 이 변경 사항에 대한 Rspec 테스트 개발을 위해 도움을 주실 수 있을까요:

app/helpers/application_helper.rb: spec/helpers/application_helper_spec.rb

여기서는 실행 가능한 단위 테스트를 찾지 못했습니다. 통합 테스트로만 테스트 가능한 것으로 보입니다.
app/models/theme.rb
app/models/theme_field.rb

QUnit Test Runner에 대해 defer 태그를 비활성화해야 했습니다: app/views/qunit/index.html.erb
이전에는 기능 플래그 "javascript defer" = false일 때만 QUnit 테스트가 실행되었습니다. 이제 "javascript defer" = true일 때도 테스트가 실행됩니다.

이것은 아마도 이미 Chrome Speed - Largest Contentful Paint Bug Fixes in M88 에서 차단되어 있을 것입니다:

배경 이미지와 시각적으로 동등한 전체 뷰포트 이미지는 더 이상 가장 큰 콘텐츠 페인트(LCP)로 간주되지 않습니다


좋은 지적입니다: https://web.dev/lcp/#how-is-an-element’s-size-determined 를 참조하세요.

텍스트 요소의 경우, 텍스트 노드의 크기만 고려됩니다(모든 텍스트 노드를 포함하는 가장 작은 직사각형).

모든 요소에 대해, CSS를 통해 적용된 margin, padding, border는 고려되지 않습니다.

  • 따라서 정적 텍스트 노드는 EmberJs 텍스트 노드와 정확히 같은 크기로 렌더링되어야 합니다.
  • 또는 line-height를 증가시켜 약간 더 크게 만들 수도 있습니다.
    예를 들어, 텍스트 노드의 폭이 일치하지 않으면 다른 줄바꿈으로 인해 정적 텍스트 노드가 EmberJs 노드보다 작아지는 많은 기하학적 경우가 발생합니다.

참조: LCP 예시


실제로 저는 토픽 페이지 내부의 게시물에 대해 noscript 렌더링을 사용했습니다. CSS 클래스가 실제 클래스와 약간 일치하므로 - 따라서 외관이 동일합니다.

참조: app/views/layouts/application.html.erb 변경 사항

수정: 제 실수였습니다. 이것은 실제로 크롤러 뷰입니다: app/views/topics/show.html.erb

POC에는 두 가지 기능이 결합되어 있습니다. 이들을 두 개의 실험적 기능 플래그로 나누는 것이 좋을까요?

  • defer 태그가 있는 JavaScript (설정 대시보드에 기능 플래그)
    (이 기능을 사용하려면 컨테이너 재빌드 또는 테마 캐시 플러시가 필요하므로 숨겨진 기능 플래그) ← 수정: 캐시를 통한 핫 스위칭
  • 토픽 뷰에서 정적 콘텐츠 표시 (설정 대시보드에 기능 플래그)

시작합니다: 기능 플래그


물론 LCP에 대한 전체적인 영향은 두 가지를 모두 사용할 때만 달성됩니다: FCP: 정적 콘텐츠

JS 디퍼링으로 인해 플러그인이나 테마 컴포넌트가 실패하는 Discourse 인스턴스가 있을 수 있습니다. 이 기능들을 분리하면, JS를 디퍼링하지 않고도 정적 콘텐츠에 대해 작은 이점을 얻을 수 있습니다: FCP: JS 디퍼링 없이 정적 콘텐츠

2022-01-30부터 POC를 적용한 Google Search Console의 첫인상:

데스크톱

데스크톱의 경우 결과가 나오기까지 시간이 좀 걸렸습니다:


참고: 이전의 녹색 기준선은 같은 도메인 내 Discourse가 아닌 웹페이지를 나타냅니다.

모바일


참고: 이전의 녹색 기준선은 같은 도메인 내 Discourse가 아닌 웹페이지를 나타냅니다.

현재 POC가 적용된 지 12일밖에 되지 않았고, 값은 지난 28일간의 평균이므로 모바일 페이지의 개선이 더 뚜렷하게 확인되기를 바라며 7~14일을 더 기다려 보겠습니다.

개념 증명(POC)에 대한 LCP 요약

POC는 2022-01-30부터 적용되었으며, CrUX 데이터에 기반한 Google Search Console의 “Core Web Vitals” 보고서에서 모든 페이지에 영향을 미치기까지 4주 이상이 소요되었습니다.

모든 주제 페이지가 LCP 녹색 구역(양호)에 속합니다 (CrUX 측정 기준):

  • 데스크톱: LCP 1.7초
  • 모바일: LCP 2.0초

LCP 데이터: Google Search Console/CrUX

2022-01-30부터 POC가 적용된 Google Search Console의 임프레션:

데스크톱


참고: 이전의 녹색 기준선은 같은 도메인 내 비-Discourse 웹페이지를 나타냅니다.

양호한 URL

모바일


참고: 이전의 녹색 기준선은 같은 도메인 내 비-Discourse 웹페이지를 나타냅니다.

양호한 URL

LCP 문제: 2.5초 초과 (모바일)


참고: 주제 페이지에서만 EmberJS 콘텐츠 전에 정적 콘텐츠가 표시됩니다.


기능 플래그가 포함된 PR 승인 필요

@sam 이 PR을 승인할 수 있도록 누군가 검토하도록 위임해 주시겠습니까?

반드시 신중하게 검토하겠습니다. 변경 사항이 매우 방대하여 검토에 시간이 조금 걸릴 수 있습니다.

@rrit 사이트에 있는 데이터를 공유해 주셔서 감사합니다! 내부에서 이 문제에 대해 논의해 보았고, 안타깝게도 현재로서는 Discourse 코어에 이 기능을 추가하지 않을 예정입니다.

공유해 주신 Web Vital 지표는 매우 인상적이지만, ‘크롤러 뷰’ 콘텐츠가 잠시 노출되는 현상은 좋은 사용자 경험을 제공하지 않습니다. 적용하신 스타일링 변경 사항은 확실히 도움이 되지만, 커스텀 스타일링이 있는 모든 Discourse 사이트마다 조정이 필요할 것입니다.

우리의 장기적인 목표는 Ember FastBoot와 같은 기술을 사용하여 진정한 서버 측 렌더링을 구현하는 것입니다. 이론적으로 이는 여러분이 측정한 것과 동일한 통계적 개선 효과를 제공하면서도 매끄러운 사용자 경험을 제공할 것입니다. 우리는 이 목표를 향해 노력을 집중하고 싶습니다.


그러나 Discourse는 확장성이 매우 뛰어나므로, 여러분의 아이디어를 Discourse 플러그인으로 구현한 후 Customization > Plugin 채널에서 공유하는 것이 충분히 가능할 것이라고 생각합니다.

코어 PR에서 가장 큰 변경 사항은 스크립트 태그에 defer 속성을 추가한 것입니다. 플러그인에서 이 모든 부분을 오버라이드하는 것은 매우 어려울 것입니다. 그러나 미들웨어 기반 접근 방식을 사용하면 동일한 결과를 달성할 수 있다고 생각합니다. 유사한 문제를 설명하는 블로그 포스트를 찾았습니다:

이 기술을 사용하면 text/html 응답을 확인하고, 이를 파싱한 후 필요한 곳에 defer 속성을 추가하는 미들웨어를 작성할 수 있습니다.

플러그인에서 미들웨어를 추가하는 방법은 다음과 같습니다:

# name: my-plugin
# about: My plugin description
# version: 1.0
# url: https://example.org

require_relative "lib/script_defer_middleware"

on(:after_initializers) do
  Rails.configuration.middleware.use(ScriptDeferMiddleware)
end

플러그인 기반 접근 방식에서 장애물에 부딪히면 여기에 게시해 주세요. 기꺼이 올바른 방향으로 안내해 드리겠습니다.

시간이 나면 플러그인을 구현할 가능성이 높습니다.

하지만 지금은 web_only.yml에서 패치 접근 방식으로 해결을 시도하고 있습니다:

# 테스트되지 않은 의사 코드!
hooks:
  after_code:
    - exec:
        cd: $home
        cmd:
          - curl https://patch-diff.githubusercontent.com/raw/discourse/discourse/pull/15858.diff | git apply

Ember FastBoot는 장기적인 접근법으로 완벽해 보입니다. 한편 LCP 주제는 여전히 뜨거운 관심을 받고 있습니다:

@rrit 님이 이 작업을 진행해 주셔서 감사합니다 :+1:

좋은 소식이 있습니다. Discourse에 새로운 기능을 구현했는데, 이 문제가 상당히 개선될 것으로 보입니다.