RenderingNG 심층 분석: BlinkNG

Stefan Zager
Stefan Zager
Chris Harrelson
Chris Harrelson

Blink는 Chromium의 웹 플랫폼 구현을 의미하며, 합성 전 렌더링의 모든 단계를 포괄하여 컴포지터 커밋으로 마무리됩니다. 깜박임 렌더링 아키텍처에 관한 자세한 내용은 이 시리즈의 이전 도움말에서 확인할 수 있습니다.

BlinkWebKit의 포크로 시작되었으며, WebKit 자체는 1998년 KHTML의 포크입니다. Chromium에서 가장 오래되고 가장 중요한 코드가 포함되어 있으며, 2014년이 되어서야 그 사용 기간이 확실히 드러났습니다. 그 해에 Blink 코드 구성과 구조에서 오랫동안 지속되어 온 결함을 해결하겠다는 목표로 BlinkNG라는 명목하에 일련의 야심 찬 프로젝트를 시작했습니다. 이 글에서는 BlinkNG와 그 구성 프로젝트, 즉 BlinkNG를 수행한 이유, 성과, 설계의 토대가 된 기본 원칙, 향후 개선의 기회를 알아봅니다.

BlinkNG 전후의 렌더링 파이프라인

NG 이전 렌더링

Blink 내의 렌더링 파이프라인은 항상 개념적으로 단계 (style, layout, Paint 등)로 분할되었지만 추상화 장벽이 누출되어 있었습니다. 개략적으로 말하자면, 렌더링과 관련된 데이터는 오래 지속되는 변경 가능한 객체로 구성됩니다. 이러한 객체는 언제든지 수정될 수 있고 수정될 수 있으며 연속적인 렌더링 업데이트에 의해 자주 재활용 및 재사용되었습니다. 다음과 같은 간단한 질문에는 신뢰할 수 있는 답을 줄 수 없었습니다.

  • 스타일, 레이아웃 또는 페인트 출력을 업데이트해야 하나요?
  • 이러한 데이터는 언제 '최종' 데이터를 값은 무엇인가요?
  • 언제 이러한 데이터를 수정해도 괜찮은가요?
  • 이 객체는 언제 삭제되나요?

여기에는 다음과 같은 여러 가지 예가 있습니다.

스타일은 스타일시트를 기반으로 ComputedStyle을 생성합니다. 그러나 ComputedStyle는 변경할 수 없습니다. 경우에 따라 이후 파이프라인 단계에서 수정될 수 있습니다.

스타일LayoutObject 트리를 생성하고 layout은 이러한 객체에 크기 및 위치 정보를 주석으로 추가합니다. 경우에 따라 layout은 트리 구조를 수정하기도 합니다. 레이아웃의 입력과 출력이 명확하게 구분되지 않았습니다.

스타일합성 과정을 결정하는 액세서리 데이터 구조를 생성하며, 이러한 데이터 구조는 스타일 이후의 모든 단계에서 수정되었습니다.

하위 수준에서 렌더링 데이터 유형은 주로 특수 트리 (예: DOM 트리, 스타일 트리, 레이아웃 트리, 페인트 속성 트리)로 구성됩니다. 렌더링 단계는 재귀 트리 워크로 구현됩니다. 트리 워크는 포함되어야 합니다. 특정 트리 노드를 처리할 때 해당 노드에 루팅된 하위 트리 외부의 정보에 액세스하면 안 됩니다. 그것은 결코 정확한 사전 렌더링이 아니었습니다. 처리 중인 노드의 상위 항목에서 자주 액세스되는 정보를 탐색합니다. 이로 인해 시스템이 매우 취약하고 오류가 발생하기 쉽습니다. 또한 나무의 뿌리 외에는 어디에서도 나무 산책을 시작할 수 없었습니다.

마지막으로, 렌더링 파이프라인에 많은 진입로가 코드 전반에 걸쳐 있었습니다. 예를 들어 JavaScript에 의해 트리거되는 강제 레이아웃, 문서 로드 중에 트리거되는 부분 업데이트, 이벤트 타겟팅을 준비하기 위한 강제 업데이트, 디스플레이 시스템에서 요청한 예약된 업데이트, 테스트 코드에만 노출되는 특수 API 등 몇 가지를 들 수 있었습니다. 렌더링 파이프라인에는 재귀재진입 경로가 몇 개 있었습니다 (즉, 다른 단계의 중간에서 한 단계의 시작으로 점프). 이러한 각 진입로는 각각 고유한 동작이 있으며 경우에 따라 렌더링 출력이 렌더링 업데이트가 트리거된 방식에 따라 달라집니다.

변경사항

BlinkNG는 앞서 설명한 아키텍처 결함을 제거한다는 공동의 목표를 가진 크고 작은 수많은 하위 프로젝트로 구성되어 있습니다. 이러한 프로젝트는 렌더링 파이프라인을 실제 파이프라인에 가깝게 만들도록 설계된 몇 가지 기본 원칙을 공유합니다.

  • 균일한 진입점: 항상 처음부터 파이프라인에 진입해야 합니다.
  • 기능 단계: 각 단계에는 잘 정의된 입력과 출력이 있어야 하며, 동작은 확정적이고 반복 가능해야 하며 기능적이어야 하며 출력은 정의된 입력에만 종속되어야 합니다.
  • 상수 입력: 스테이지가 실행되는 동안 모든 단계의 입력이 사실상 일정해야 합니다.
  • 변경 불가능한 출력: 단계가 완료되면 렌더링 업데이트의 나머지 기간 동안 출력을 변경할 수 없어야 합니다.
  • 체크포인트 일관성: 각 단계가 끝날 때 지금까지 생성된 렌더링 데이터는 자체 일관성 상태여야 합니다.
  • 작업 중복 삭제: 각 항목을 한 번만 계산합니다.

BlinkNG 하위 프로젝트의 전체 목록은 읽는 데 지루할 수 있지만 다음과 같은 몇 가지 구체적인 결과가 있습니다.

문서 수명 주기

DocumentLifecycle 클래스는 렌더링 파이프라인을 통해 진행 상황을 추적합니다. 이를 통해 앞에서 나열된 불변 항목을 적용하는 다음과 같은 기본 검사를 수행할 수 있습니다.

  • ComputedStyle 속성을 수정하는 경우 문서 수명 주기는 kInStyleRecalc여야 합니다.
  • DocumentLifecycle 상태가 kStyleClean 이상이면 NeedsStyleRecalc()는 연결된 모든 노드에 대해 false를 반환해야 합니다.
  • 페인트 수명 주기 단계에 진입할 때 수명 주기 상태는 kPrePaintClean이어야 합니다.

BlinkNG를 구현하는 과정에서 우리는 이러한 불변 항목을 위반하는 코드 경로를 체계적으로 제거하고 코드 전체에 더 많은 어설션을 스프링하여 회귀를 방지했습니다.

낮은 수준의 렌더링 코드를 살펴보면서 '어떻게 여기까지 왔지?'라고 자문해 본 적이 있을 것입니다. 앞서 언급했듯이 렌더링 파이프라인에 진입하는 다양한 진입점이 있습니다. 이전에는 재귀 및 재진입 호출 경로와 처음부터 시작하는 것이 아니라 중간 단계에서 파이프라인에 진입한 위치가 포함되었습니다. BlinkNG 과정에서 이러한 호출 경로를 분석하여 두 가지 기본 시나리오로 모두 축소할 수 있음을 확인했습니다.

  • 디스플레이를 위한 새 픽셀을 생성하거나 이벤트 타겟팅을 위한 조회 테스트를 수행하는 경우 등 모든 렌더링 데이터를 업데이트해야 합니다.
  • 모든 렌더링 데이터를 업데이트하지 않고도 답변할 수 있는 특정 쿼리에 대한 최신 값이 필요합니다. 여기에는 대부분의 JavaScript 쿼리(예: node.offsetTop)가 포함됩니다.

이제 렌더링 파이프라인에 들어가는 진입점은 두 개뿐이며, 이 두 시나리오에 해당합니다. 재진입 코드 경로가 삭제되거나 리팩터링되었으며 더 이상 중간 단계에서부터 파이프라인에 진입할 수 없습니다. 덕분에 렌더링 업데이트가 정확히 언제 어떻게 이루어지는지에 대한 수많은 미스터리가 없어져서 시스템 동작에 대해 훨씬 더 쉽게 추론할 수 있게 되었습니다.

배관 스타일, 레이아웃, 사전 페인트

총체적으로 페인트 이전의 렌더링 단계는 다음을 담당합니다.

  • 스타일 캐스케이드 알고리즘을 실행하여 DOM 노드의 최종 스타일 속성을 계산합니다.
  • 문서의 상자 계층 구조를 나타내는 레이아웃 트리를 생성합니다.
  • 모든 상자의 크기 및 위치 정보를 확인합니다.
  • 페인트를 위해 하위 픽셀 도형을 전체 픽셀 경계에 반올림 또는 스냅
  • 합성된 레이어의 속성 (아핀 변환, 필터, 불투명도 또는 GPU 가속이 가능한 기타 항목) 확인
  • 이전 페인트 단계 이후 변경된 콘텐츠를 확인하여 페인트하거나 다시 페인트해야 합니다 (페인트 무효화).

이 목록은 변경되지 않았지만, BlinkNG 이전에는 이 작업의 대부분이 임시 방식으로 수행되었으며 다수의 렌더링 단계에 걸쳐 분산되어 있고 많은 중복 기능과 기본적으로 비효율성이 있었습니다. 예를 들어 style 단계는 항상 노드의 최종 스타일 속성을 주로 계산해 왔지만 style 단계가 완료될 때까지 최종 스타일 속성 값을 결정하지 않은 몇 가지 특수한 사례도 있었습니다. 렌더링 프로세스에서 스타일 정보가 완전하고 변경 불가능하다고 확실하게 말할 수 있는 공식적인 시점은 없었습니다.

BlinkNG 문제의 또 다른 좋은 예는 페인트 무효화입니다. 이전에는 페인트에 이르기까지 모든 렌더링 단계에서 페인트 무효화가 발생했습니다. 스타일 또는 레이아웃 코드를 수정할 때 페인트 무효화 로직에 어떤 변경사항이 필요한지 알기가 어려웠으며, 실수로 무효화 또는 과잉 무효화 버그를 일으키는 실수를 저지르기도 했습니다. LayoutNG에 중점을 둔 이 시리즈의 문서에서 기존 페인트 무효화 시스템의 복잡성에 관해 자세히 알아볼 수 있습니다.

페인트를 위해 서브픽셀 레이아웃 도형을 전체 픽셀 경계에 맞추기는 동일한 기능을 여러 번 구현하고 많은 중복 작업을 실행한 예입니다. 페인트 시스템에서 사용하는 픽셀 스냅 코드 경로가 하나 있었고, 페인트 코드 외부의 픽셀 스냅된 좌표를 일회성으로 즉석에서 계산해야 할 때마다 완전히 별도의 코드 경로가 사용되었습니다. 당연한 말이지만, 구현마다 고유한 버그가 있었으며 결과가 항상 일치하지는 않았습니다. 이 정보를 캐시하지 않았기 때문에 시스템은 때때로 똑같은 계산을 반복적으로 수행했으며 이는 또 다른 성능 저하의 원인이었습니다.

다음은 페인트 전에 렌더링 단계의 아키텍처적 결함을 제거한 중요한 프로젝트입니다.

Project Squad: 스타일 단계 파이프라인 라인

이 프로젝트에서는 스타일 단계의 두 가지 주요 결함을 해결하여 파이프라인을 깔끔하게 구성했습니다.

스타일 단계에는 두 가지 기본 출력이 있습니다. ComputedStyle에는 DOM 트리를 통해 CSS 캐스케이드 알고리즘을 실행한 결과가 포함됩니다. 레이아웃 단계의 작업 순서를 설정하는 LayoutObjects 트리가 있습니다. 개념적으로 캐스케이드 알고리즘 실행은 레이아웃 트리를 생성하기 전에 엄격하게 이루어져야 합니다. 이전에는 이 두 연산이 인터리브 처리되었습니다. Project Squad는 이 두 가지를 별개의 순차적 단계로 나누는 데 성공했습니다.

이전에는 ComputedStyle가 스타일을 다시 계산하는 동안 최종 값을 가져오지 못한 경우도 있었습니다. 이후 파이프라인 단계에서 ComputedStyle가 업데이트된 몇 가지 상황이 있었습니다. Project Squad는 이러한 코드 경로를 성공적으로 리팩터링하여 스타일 단계 후에 ComputedStyle가 수정되지 않도록 했습니다.

LayoutNG: 레이아웃 단계 파이프라이닝

renderNG의 초석 중 하나인 이 기념비적인 프로젝트는 레이아웃 렌더링 단계를 완전히 재작성한 것입니다. 여기서는 프로젝트 전체를 간결하게 정의하지는 않지만 전반적인 BlinkNG 프로젝트에 대해 몇 가지 주목할 만한 측면이 있습니다.

  • 이전에는 레이아웃 단계에서 스타일 단계에서 생성된 LayoutObject 트리가 수신되었으며 트리에 크기 및 위치 정보가 주석으로 처리되었습니다. 따라서 출력과 입력이 명확하게 분리되지 않았습니다. LayoutNG는 레이아웃의 기본 읽기 전용 출력인 프래그먼트 트리를 도입했으며 후속 렌더링 단계에서 기본 입력 역할을 합니다.
  • LayoutNG는 containment 속성을 레이아웃에 도입했습니다. 특정 LayoutObject의 크기와 위치를 계산할 때 더 이상 해당 객체에 루팅된 하위 트리 외부를 보지 않습니다. 지정된 객체의 레이아웃을 업데이트하는 데 필요한 모든 정보는 미리 계산되어 알고리즘에 대한 읽기 전용 입력으로 제공됩니다.
  • 이전에는 레이아웃 알고리즘이 엄격하게 작동하지 않는 극단적인 사례가 있었습니다. 즉, 알고리즘의 결과는 가장 최근의 이전 레이아웃 업데이트에 종속되었습니다. LayoutNG는 이러한 사례를 제거했습니다.

페인트 전 단계

이전에는 공식적인 사전 페인트 렌더링 단계가 없었고 레이아웃 후 작업을 그랩 백에 사용했습니다. 사전 페인트 단계는 레이아웃이 완료된 후 레이아웃 트리의 체계적인 순회로 가장 잘 구현할 수 있는 몇 가지 관련 함수가 있다는 인식에서 비롯되었습니다. 가장 중요한 사항:

  • 페인트 무효화 실행: 정보가 불완전하면 레이아웃 과정 중에 페인트 무효화를 올바르게 실행하기가 매우 어렵습니다. 두 개의 개별 프로세스로 분할되는 경우 훨씬 쉽게 맞고 매우 효율적일 수 있습니다. 스타일과 레이아웃 중에 콘텐츠를 간단한 불리언 플래그로 '페인트 무효화 필요 가능'으로 표시할 수 있습니다. Google에서는 페인트를 칠하기 전 트리 워크를 수행하는 동안 이러한 플래그를 확인하고 필요에 따라 무효화를 실행합니다.
  • 페인트 속성 트리 생성: 자세히 설명된 프로세스입니다.
  • 픽셀 맞추기 페인트 위치 계산 및 기록: 기록된 결과는 페인트 단계에서 사용할 수 있으며 중복 계산 없이 페인트를 필요로 하는 모든 다운스트림 코드에서도 사용할 수 있습니다.

속성 트리: 일관된 도형

속성 트리는 웹에서 다른 모든 시각 효과와 구조가 다른 스크롤의 복잡성을 해결하기 위해 렌더링의 초기에 도입되었습니다. 속성 트리 이전에는 Chromium의 컴포지터가 단일 '레이어'를 사용함 계층 구조를 사용합니다. 레이어 계층 구조가 '스크롤 상위 요소'를 나타내는 추가 비로컬 포인터를 증가시킴 또는 'clip 상위' 얼마 지나지 않아 코드를 이해하기가 매우 어려웠습니다.

속성 트리는 콘텐츠의 오버플로 스크롤 및 클립 측면을 다른 모든 시각 효과와 별도로 표시하여 이 문제를 해결했습니다. 이를 통해 웹사이트의 실제 시각적 구조와 스크롤 구조를 올바르게 모델링할 수 있었습니다. 다음은 '모두'입니다. 속성 트리 위에 합성된 레이어의 화면 공간 변환 같은 알고리즘을 구현하거나 스크롤된 레이어와 스크롤하지 않은 레이어를 결정해야 했습니다.

사실, 저희는 곧 코드에서 유사한 기하학적 질문이 많이 발생한 다른 곳이 있다는 사실을 알게 되었습니다. (주요 데이터 구조 게시물에서 더 많은 항목을 확인할 수 있습니다.) 그 중 일부는 컴포지터 코드가 하는 것과 동일한 작업을 중복해서 구현했습니다. 버그의 하위 집합이 달랐습니다. 이들 모두 실제 웹사이트 구조를 올바르게 모델링하지 못했습니다 이에 대한 솔루션은 명확해졌습니다. 모든 기하학 알고리즘을 한 곳에 집중시키고 모든 코드를 리팩터링하여 사용할 수 있도록 하는 것입니다.

이러한 알고리즘은 모두 속성 트리에 의존합니다. 따라서 속성 트리는 renderNG의 데이터 구조(즉, 파이프라인 전체에서 사용되는 데이터 구조)입니다. 따라서 중앙 집중식 도형 코드라는 목표를 달성하기 위해서는 파이프라인 훨씬 초기에 속성 트리 개념을 사전 페인트에 도입하고 현재 속성에 의존하는 모든 API를 변경해야 실행 전에 사전 페인트를 실행해야 했습니다.

이 사례는 BlinkNG 리팩터링 패턴의 또 다른 측면입니다. 주요 계산을 식별하고, 중복을 피하도록 리팩터링하고, 이를 피드하는 데이터 구조를 생성하는 잘 정의된 파이프라인 단계를 생성합니다. Google은 필요한 모든 정보를 사용할 수 있는 시점에 속성 트리를 계산합니다. 이후 렌더링 단계가 실행되는 동안 속성 트리가 변경되지 않도록 합니다.

페인트 후 합성: 배관 페인트 및 합성

계층화는 어떤 DOM 콘텐츠가 자체 합성된 레이어에 들어가는지 (결과적으로 GPU 텍스처를 나타냄) 파악하는 프로세스입니다. RenderNG 전에는 레이어화가 페인트 후가 아니라 페인트 전에 실행되었습니다 (현재 파이프라인은 여기 참고, 순서 변경에 유의). 우리는 먼저 DOM의 어느 부분이 합성된 레이어로 들어가는지 결정한 다음, 그런 다음에 해당 텍스처에 대한 표시 목록을 그립니다. 당연히 결정은 애니메이션 또는 스크롤 중인 DOM 요소, 3D 변환, 그 위에 그려진 요소 등의 요소에 따라 결정되었습니다.

코드에 순환 종속 항목이 있어야 할 필요성이 어느 정도 필요했기 때문에 심각한 문제가 발생했습니다. 이는 렌더링 파이프라인에 큰 문제입니다. 예를 통해 그 이유를 알아보겠습니다. 페인트를 무효화해야 한다고 가정해 보겠습니다. 즉, 표시 목록을 다시 그린 다음 다시 래스터화해야 합니다. DOM 변경 또는 변경된 스타일이나 레이아웃으로 인해 무효화할 수 있습니다. 물론 실제로 변경된 부분만 무효화하려고 합니다. 즉, 영향을 받은 합성된 레이어를 찾은 다음 해당 레이어에 대한 표시 목록의 일부 또는 전체를 무효화했습니다.

즉, 무효화가 DOM, 스타일, 레이아웃, 이전 계층화 결정 (이전: 이전에 렌더링된 프레임의 의미)에 따라 달라졌습니다. 그러나 현재의 계층화는 이러한 모든 요소에 따라 달라집니다. 또한 모든 계층화 데이터의 복사본을 두 개 보유하지 않았기 때문에 과거와 미래의 계층화 결정을 구분하기 어려웠습니다. 그래서 순환 추론을 사용하는 코드를 많이 만들었습니다. 이로 인해 비논리적이거나 잘못된 코드가 생성되기도 하고, 조심하지 않으면 비정상 종료나 보안 문제가 발생하기도 했습니다.

이 상황을 처리하기 위해 DisableCompositingQueryAsserts 객체의 개념을 일찍 소개했습니다. 대부분의 경우 코드가 이전 계층화 결정을 쿼리하려고 하면 어설션 실패가 발생하고 브라우저가 디버그 모드인 경우 브라우저가 비정상 종료됩니다. 덕분에 새로운 버그가 발생하는 것을 막을 수 있었습니다. 또한 이전 계층화 결정을 쿼리하는 데 합법적으로 코드가 필요한 각 경우에는 DisableCompositingQueryAsserts 객체를 할당하여 이를 허용하도록 코드를 삽입합니다.

시간이 지남에 따라 모든 호출 사이트 DisableCompositingQueryAssert 객체를 삭제한 후 코드를 안전하고 정확하다고 선언하는 것이 계획되었습니다. 그러나 저희가 발견한 것은 페인트 전에 레이어화가 발생하는 한 다수의 호출이 근본적으로 제거할 수 없다는 것입니다. (마침내 아주 최근에 삭제가 되었습니다.) 이것이 페인트 후 복합 프로젝트가 발견된 첫 번째 이유입니다. 우리는 작업에 대해 잘 정의된 파이프라인 단계가 있더라도 파이프라인에서 잘못된 위치에 있으면 결국 멈추게 된다는 사실을 알게 되었습니다.

Composite After Paint 프로젝트의 두 번째 이유는 Fundamental Compositing 버그 때문입니다. 이 버그를 언급하는 한 가지 방법은 DOM 요소가 웹페이지 콘텐츠를 위한 효율적이거나 완전한 계층화 스킴의 적절한 1:1 표현이 아니라는 것입니다. 그리고 합성은 페인트 전에 이루어졌으므로 본질적으로 표시 목록이나 속성 트리가 아닌 DOM 요소에 거의 의존했습니다. 이는 속성 트리를 도입한 이유와 매우 유사하며, 속성 트리와 마찬가지로 올바른 파이프라인 단계를 알아내고 적절한 시점에 실행하고 올바른 주요 데이터 구조를 제공하면 솔루션이 직접적으로 영향을 받습니다. 속성 트리와 마찬가지로 이는 페인트 단계가 완료되면 후속 모든 파이프라인 단계에서 출력을 변경할 수 없음을 보장하기에 좋은 기회였습니다.

이점

보시다시피 잘 정의된 렌더링 파이프라인은 장기적으로 엄청난 이점을 가져다줍니다. 생각보다 훨씬 많은 것이 있습니다.

  • 안정성 대폭 개선: 이 방법은 매우 간단합니다. 잘 정의되고 이해하기 쉬운 인터페이스를 갖춘 더 깔끔한 코드는 이해, 작성, 테스트하기가 더 쉽습니다. 이렇게 하면 안정성이 향상됩니다. 또한 비정상 종료와 use-after-free 버그가 감소하여 코드가 더 안전하고 안정적으로 이루어집니다.
  • 테스트 범위 확장: BlinkNG 과정에서 새로운 테스트를 많이 추가했습니다. 여기에는 내부 요소에 대한 집중적인 확인을 제공하는 단위 테스트가 포함됩니다. 이전에 수정했던 버그를 다시 도입하지 못하게 하는 회귀 테스트 모든 브라우저가 웹 표준 준수를 측정하는 데 사용하는 공동으로 유지관리하는 웹 플랫폼 테스트 도구 모음(일반 안정화 버전)의 많은 도구를 살펴보세요.
  • 더 쉬운 확장: 시스템이 명확한 구성요소로 구분되어 있는 경우 현재 구성요소로 진행하기 위해 다른 구성요소를 세부적으로 이해할 필요가 없습니다. 이렇게 하면 심층적인 전문가가 아니어도 모든 사람이 렌더링 코드에 더 쉽게 가치를 더할 수 있으며 전체 시스템의 동작에 대해 더 쉽게 추론할 수 있습니다.
  • 성능: 스파게티 코드로 작성된 알고리즘을 최적화하는 것은 매우 어렵지만 이러한 파이프라인 없이는 범용 스레드 스크롤 및 애니메이션이나 사이트 격리를 위한 프로세스 및 스레드와 같은 더 큰 목표를 달성하는 것이 거의 불가능합니다. 동시 로드는 성능을 크게 개선하는 데 도움이 될 수 있지만 매우 복잡합니다.
  • 양보 및 억제: BlinkNG는 파이프라인을 새롭고 참신한 방식으로 실행하는 몇 가지 새로운 기능을 제공합니다. 예를 들어 예산이 만료될 때까지만 렌더링 파이프라인을 실행하려면 어떻게 해야 할까요? 아니면 현재 사용자와 관련이 없는 것으로 알려진 하위 트리의 렌더링을 건너뛰나요? 이것이 content- visibility CSS 속성을 사용하면 가능합니다. 구성요소의 스타일이 레이아웃에 종속되도록 하는 것은 어떨까요? 이것이 바로 컨테이너 쿼리입니다.

우수사례: 컨테이너 쿼리

컨테이너 쿼리는 출시 예정인 웹 플랫폼 기능으로, 수년간 CSS 개발자들이 가장 많이 요청한 기능입니다. 이렇게 훌륭한데 아직 존재하지 않는 이유는 무엇인가? 컨테이너 쿼리를 구현하려면 스타일과 레이아웃 코드 간의 관계를 매우 신중하게 이해하고 제어해야 하기 때문입니다. 좀 더 자세히 살펴보겠습니다.

컨테이너 쿼리를 사용하면 요소에 적용되는 스타일이 상위 요소의 배치 크기에 종속될 수 있습니다. 레이아웃된 크기는 레이아웃 중에 계산되므로 레이아웃 후에 스타일 재계산을 실행해야 합니다. 그러나 레이아웃보다 먼저 스타일 재계산이 실행됩니다. 이 닭과 계란의 역설은 BlinkNG 이전에 컨테이너 쿼리를 구현할 수 없었던 완전한 이유입니다.

이 문제를 어떻게 해결할 수 있을까요? 역방향 파이프라인 종속 항목이 아닌가요? 즉, Paint After Paint와 같은 프로젝트가 해결한 것과 동일한 문제가 아닌가요? 더 나쁜 것은, 새로운 스타일로 인해 상위 항목의 크기가 변경되면 어떻게 될까요? 가끔 무한 루프가 발생하지 않나요?

원칙적으로 순환 종속 항목은 요소 외부 렌더링을 해당 요소의 하위 트리 내 렌더링에 의존하지 않도록 허용하는 include CSS 속성을 사용하여 해결할 수 있습니다. 즉, 컨테이너 쿼리에는 포함이 필요하기 때문에 컨테이너가 적용하는 새로운 스타일은 컨테이너 크기에 영향을 미칠 수 없습니다.

하지만 사실 이것으로는 충분하지 않았습니다. 규모를 막는 것보다 더 약한 유형의 격리를 도입해야 했습니다. 컨테이너 쿼리 컨테이너가 인라인 크기에 따라 한 방향 (일반적으로 차단)으로만 크기를 조절할 수 있도록 하는 것이 일반적이기 때문입니다. 따라서 인라인 크기 포함의 개념이 추가되었습니다. 그러나 해당 섹션의 매우 긴 메모에서 알 수 있듯이 인라인 크기 포함이 가능한지 여부가 오랫동안 명확하지 않았습니다.

포함을 추상 사양 언어로 설명하는 것과 이를 올바르게 구현하는 것은 또 다른 일입니다. BlinkNG의 목표 중 하나는 렌더링의 기본 로직을 구성하는 트리 워크에 억제 원칙을 적용하는 것이었습니다. 즉, 하위 트리를 순회할 때 하위 트리 외부에서 정보가 필요하지 않아야 합니다. 이런 일이 발생하면 (물론 우연은 아니었음) 렌더링 코드가 포함 원칙을 준수하면 훨씬 더 깔끔하고 쉽게 CSS 포함을 구현할 수 있습니다.

향후: 기본 스레드를 벗어난 합성 등 그 이상

여기에 표시된 렌더링 파이프라인은 실제로 현재 RenderingNG 구현보다 약간 앞서 있습니다. 계층화가 기본 스레드를 벗어나는 것으로 표시되지만 현재는 여전히 기본 스레드에 있습니다. 그러나 이제 페인트 후 합성이 제공되고 페인트 후에 레이어화가 완료되므로 이 작업이 완료되기까지는 시간 문제가 없습니다.

이것이 왜 중요한지, 다른 무엇으로 이어질 수 있는지 이해하려면 렌더링 엔진의 아키텍처를 좀 더 높은 관점에서 고려해야 합니다. Chromium의 성능을 개선하는 데 있어 가장 지속 가능한 장애물 중 하나는 렌더러의 기본 스레드가 기본 애플리케이션 로직 (즉, 스크립트 실행)과 대규모 렌더링을 모두 처리한다는 단순한 사실입니다. 따라서 기본 스레드는 작업으로 포화 상태가 되는 경우가 많고, 기본 스레드의 혼잡으로 인해 전체 브라우저에서 병목 현상이 발생하는 경우가 많습니다.

좋은 소식은 이렇게 하지 않아도 된다는 것입니다. Chromium 아키텍처의 이러한 측면은 단일 스레드 실행이 주요 프로그래밍 모델이었던 KHTML 시절까지 거슬러 올라갑니다. 멀티코어 프로세서가 소비자급 기기에서 일반화될 무렵에는 단일 스레드 가정이 Blink (이전의 WebKit)에 철저히 통합되었습니다. 오랫동안 렌더링 엔진에 더 많은 스레딩을 도입하려고 했지만 기존 시스템에서는 불가능했습니다. NG 렌더링의 주요 목표 중 하나는 이 허점을 찾아 렌더링 작업의 일부 또는 전체를 다른 스레드 (또는 스레드)로 이동하는 것이었습니다.

BlinkNG가 곧 완공됨에 따라 이미 이 영역을 연구하기 시작했습니다. Non-Blocking Commit은 렌더기의 스레딩 모델을 변경하는 첫 번째 시도입니다. 컴포지터 커밋 (또는 단순히 커밋)은 기본 스레드와 컴포지터 스레드 간의 동기화 단계입니다. 커밋하는 동안 기본 스레드에서 생성된 렌더링 데이터의 사본을 만들어 컴포지터 스레드에서 실행되는 다운스트림 합성 코드에서 사용합니다. 이 동기화가 진행되는 동안에는 코드 복사가 컴포지터 스레드에서 실행되는 동안 기본 스레드 실행이 중지됩니다. 이는 컴포지터 스레드가 렌더링 데이터를 복사하는 동안 기본 스레드가 렌더링 데이터를 수정하지 않도록 하기 위한 것입니다.

비차단 커밋을 사용하면 기본 스레드를 중지하고 커밋 단계가 종료될 때까지 기다릴 필요가 없습니다. 즉, 기본 스레드는 컴포지터 스레드에서 커밋이 동시에 실행되는 동안 작업을 계속합니다. 비차단 커밋을 사용하면 기본 스레드에서 렌더링 작업만 수행할 수 있는 시간이 줄어들어 기본 스레드의 정체가 줄어들고 성능이 개선됩니다. 이 문서 (2022년 3월)를 기준으로, 비차단 커밋의 작업 프로토타입이 준비되었으며, 성능에 미치는 영향에 대한 자세한 분석을 준비하고 있습니다.

그보다는 기본 스레드 외부 합성이 진행 중이며, 이 목표는 계층화를 기본 스레드에서 작업자 스레드로 이동하여 렌더링 엔진이 삽화와 일치하도록 만드는 것입니다. Non-Blocking Commit과 마찬가지로 렌더링 워크로드를 줄여 기본 스레드의 정체를 줄입니다. Composite After Paint의 아키텍처 개선이 없었다면 이러한 프로젝트는 불가능했을 것입니다.

그리고 더 많은 프로젝트가 파이프라인에 있습니다 (장난꾸러기 목적). 마침내 렌더링 작업의 재배포를 실험할 수 있는 토대를 갖추게 되었습니다. 실현할 수 있을지 무척 기대됩니다.