Blink는 Chromium의 웹 플랫폼 구현을 의미하며, 컴포지팅 전의 모든 렌더링 단계를 포함하며 컴포저 커밋으로 끝납니다. Blink 렌더링 아키텍처에 관한 자세한 내용은 이 시리즈의 이전 도움말을 참고하세요.
Blink는 1998년에 탄생한 KHTML의 포크인 WebKit의 포크로 시작되었습니다. 여기에는 Chromium에서 가장 오래되고 가장 중요한 코드가 포함되어 있으며 2014년에는 확실히 오래된 코드임을 보여주었습니다. 그해 Google은 Blink 코드의 구성 및 구조에 오래된 결함을 해결하기 위해 BlinkNG라는 이름으로 야심 찬 프로젝트를 시작했습니다. 이 도움말에서는 BlinkNG와 그 구성 프로젝트(구현 이유, 달성한 목표, 설계를 형성한 원칙, 향후 개선 기회)를 살펴봅니다.
NG 이전 렌더링
Blink 내의 렌더링 파이프라인은 항상 개념적으로 단계 (스타일, 레이아웃, 페인트 등)로 분할되었지만 추상화 장벽은 새기기 쉽습니다. 대략적으로 렌더링과 관련된 데이터는 변경 가능한 장기 실행 객체로 구성되었습니다. 이러한 객체는 언제든지 수정할 수 있었으며, 실제로 수정되었으며, 연속적인 렌더링 업데이트에서 자주 재활용되고 재사용되었습니다. 다음과 같은 간단한 질문에 확실하게 답변할 수 없었습니다.
- 스타일, 레이아웃 또는 페인트의 출력을 업데이트해야 하나요?
- 이러한 데이터는 언제 '최종' 값을 가져오나요?
- 언제 이러한 데이터를 수정해도 되나요?
- 이 객체는 언제 삭제되나요?
다음과 같은 여러 가지 예가 있습니다.
스타일은 스타일시트를 기반으로 ComputedStyle
를 생성합니다. 하지만 ComputedStyle
는 불변이 아니었습니다. 경우에 따라 이후 파이프라인 단계에서 수정될 수 있습니다.
스타일은 LayoutObject
의 트리를 생성하고 레이아웃은 크기 및 위치 정보로 이러한 개체에 주석을 추가합니다. 경우에 따라 레이아웃이 트리 구조를 수정하기도 합니다. 레이아웃의 입력과 출력이 명확하게 구분되지 않았습니다.
스타일은 조합의 과정을 결정하는 보조 데이터 구조를 생성하며, 이러한 데이터 구조는 스타일 이후의 모든 단계에서 제자리에서 수정되었습니다.
하위 수준에서 렌더링 데이터 유형은 대부분 특수화된 트리 (예: DOM 트리, 스타일 트리, 레이아웃 트리, 페인트 속성 트리)로 구성되며 렌더링 단계는 재귀 트리 워크로 구현됩니다. 이상적으로 트리 워크는 포함되어야 합니다. 즉, 특정 트리 노드를 처리할 때 해당 노드에 루팅된 하위 트리 외부의 정보에 액세스해서는 안 됩니다. RenderingNG 이전에는 이 말이 사실이 아니었습니다. 트리 워크는 처리 중인 노드의 조상에서 정보에 자주 액세스했습니다. 이로 인해 시스템이 매우 취약하고 오류가 발생하기 쉬웠습니다. 또한 트리의 루트가 아닌 곳에서 트리 워크를 시작할 수 없습니다.
마지막으로 코드 전체에 렌더링 파이프라인으로 진입하는 여러 경로가 있었습니다. 예를 들면 JavaScript에 의해 트리거된 강제 레이아웃, 문서 로드 중에 트리거된 부분 업데이트, 이벤트 타겟팅을 준비하기 위한 강제 업데이트, 디스플레이 시스템에서 요청한 예약된 업데이트, 테스트 코드에만 노출되는 특수 API 등이 있습니다. 렌더링 파이프라인으로 들어가는 재귀 및 재진입 경로 (즉, 한 단계의 중간에서 다른 단계의 시작으로 점프)도 몇 개 있었습니다. 이러한 온램프에는 각각 고유한 동작이 있으며, 경우에 따라 렌더링 업데이트가 트리거된 방식에 따라 렌더링 출력이 달라질 수 있습니다.
변경된 사항
BlinkNG는 크고 작은 여러 하위 프로젝트로 구성되어 있으며, 모두 앞에서 설명한 아키텍처 결함을 제거하는 공통된 목표를 가지고 있습니다. 이러한 프로젝트는 렌더링 파이프라인을 실제 파이프라인에 더 가깝게 만들기 위해 설계된 몇 가지 원칙을 공유합니다.
- 통일된 진입점: 항상 파이프라인의 시작 부분에서 진입해야 합니다.
- 기능적 단계: 각 단계에는 잘 정의된 입력과 출력이 있어야 하며, 동작은 기능적이어야 합니다. 즉, 결정론적이고 반복 가능해야 하며 출력은 정의된 입력에만 종속되어야 합니다.
- 상수 입력: 모든 단계의 입력은 단계가 실행되는 동안 효과적으로 상수여야 합니다.
- 변경 불가능한 출력: 한 단계가 완료되면 나머지 렌더링 업데이트 동안 출력을 변경할 수 없어야 합니다.
- 체크포인트 일관성: 각 단계가 끝날 때까지 지금까지 생성된 렌더링 데이터는 자체 일관된 상태여야 합니다.
- 작업 중복 삭제: 각 항목을 한 번만 계산합니다.
BlinkNG 하위 프로젝트의 전체 목록은 지루하게 느껴질 수 있지만 다음은 특히 중요한 몇 가지 프로젝트입니다.
문서 수명 주기
DocumentLifecycle 클래스는 렌더링 파이프라인을 통한 진행 상황을 추적합니다. 이를 통해 앞서 나열된 불변식을 적용하는 기본 검사(예:
- ComputedStyle 속성을 수정하는 경우 문서 수명 주기는
kInStyleRecalc
여야 합니다. - DocumentLifecycle 상태가
kStyleClean
이상이면NeedsStyleRecalc()
는 연결된 노드에 대해 false를 반환해야 합니다. - 페인트 수명 주기 단계에 진입할 때 수명 주기 상태는
kPrePaintClean
여야 합니다.
BlinkNG를 구현하는 과정에서 이러한 불변식을 위반하는 코드 경로를 체계적으로 제거하고 코드 전체에 더 많은 어설션을 추가하여 회귀하지 않도록 했습니다.
하위 수준 렌더링 코드를 살펴보면서 '어쩌다 여기까지 왔지?'라고 생각해 본 적이 있을 것입니다. 앞서 언급했듯이 렌더링 파이프라인에는 다양한 진입점이 있습니다. 이전에는 재귀 호출 경로와 재진입 호출 경로, 처음부터 시작하는 대신 중간 단계에서 파이프라인에 진입한 위치가 여기에 포함되었습니다. BlinkNG 과정에서 이러한 호출 경로를 분석한 결과, 모두 다음 두 가지 기본 시나리오로 축소할 수 있음을 확인했습니다.
- 디스플레이용 새 픽셀을 생성하거나 이벤트 타겟팅을 위한 히트 테스트를 실행할 때와 같이 모든 렌더링 데이터를 업데이트해야 합니다.
- 모든 렌더링 데이터를 업데이트하지 않고도 답변할 수 있는 특정 쿼리의 최신 값이 필요합니다. 여기에는 대부분의 JavaScript 쿼리(예:
node.offsetTop
)가 포함됩니다.
이제 렌더링 파이프라인의 진입점은 두 시나리오에 해당하는 두 개뿐입니다. 재진입 코드 경로가 삭제되거나 리팩터링되었으며 더 이상 중간 단계에서 시작하여 파이프라인에 진입할 수 없습니다. 이를 통해 렌더링 업데이트가 정확히 언제 어떻게 발생하는지에 관한 많은 미스터리가 해결되었으며, 시스템 동작에 관해 훨씬 더 쉽게 추론할 수 있게 되었습니다.
스타일, 레이아웃, 사전 페인팅 파이프라인
페인트 전에 발생하는 렌더링 단계는 다음을 담당합니다.
- 스타일 계단식 적용 알고리즘을 실행하여 DOM 노드의 최종 스타일 속성을 계산합니다.
- 문서의 상자 계층 구조를 나타내는 레이아웃 트리를 생성합니다.
- 모든 상자의 크기 및 위치 정보를 결정합니다.
- 페인팅을 위해 하위 픽셀 도형을 전체 픽셀 경계로 반올림하거나 스냅합니다.
- 합성된 레이어의 속성 (아핀 변환, 필터, 불투명도 또는 GPU 가속이 가능한 기타 항목)을 결정합니다.
- 이전 페인트 단계 이후 변경되어 페인트 또는 다시 페인트해야 하는 콘텐츠를 결정합니다 (페인트 무효화).
이 목록은 변경되지 않았지만 BlinkNG 이전에는 이러한 작업의 대부분이 여러 렌더링 단계에 걸쳐 임시로 이루어졌으며 중복된 기능과 내장된 비효율성이 많았습니다. 예를 들어 스타일 단계는 항상 노드의 최종 스타일 속성을 계산하는 데 주로 관여했지만, 스타일 단계가 완료된 후에야 최종 스타일 속성 값을 결정하지 않는 몇 가지 특수한 경우가 있었습니다. 렌더링 프로세스에는 스타일 정보가 완전하고 변경 불가능하다고 확실하게 말할 수 있는 공식적이거나 시행 가능한 지점이 없었습니다.
BlinkNG 이전 문제의 또 다른 좋은 예는 페인트 무효화입니다. 이전에는 페인트로 이어지는 모든 렌더링 단계에 페인트 무효화가 흩어져 있었습니다. 스타일 또는 레이아웃 코드를 수정할 때 페인트 무효화 로직에 어떤 변경사항이 필요한지 파악하기 어려웠으며 실수를 하여 무효화 부족 또는 과잉 버그가 발생하기 쉽습니다. 이전 페인트 무효화 시스템의 복잡성에 관한 자세한 내용은 이 시리즈의 LayoutNG 관련 도움말을 참고하세요.
페인팅을 위해 하위 픽셀 레이아웃 도형을 전체 픽셀 경계로 스냅하는 것은 동일한 기능을 여러 번 구현하고 많은 중복 작업을 한 예입니다. 페인트 시스템에서 사용하는 하나의 픽셀 스냅 코드 경로와 페인트 코드 외부에서 일회성으로 픽셀 스냅된 좌표를 실시간으로 계산해야 할 때마다 사용되는 완전히 별개의 코드 경로가 있었습니다. 당연히 각 구현에는 자체 버그가 있었고 결과가 항상 일치하지는 않았습니다. 이 정보는 캐시되지 않았기 때문에 시스템이 동일한 계산을 반복적으로 실행하는 경우가 있었습니다. 이는 성능에 또 다른 부담이 됩니다.
다음은 페인트하기 전에 렌더링 단계의 아키텍처 적자를 제거한 몇 가지 중요한 프로젝트입니다.
Project Squad: 스타일 단계 파이프라인
이 프로젝트에서는 스타일 단계에서 파이프라인을 깔끔하게 구성하는 데 방해가 되는 두 가지 주요 문제를 해결했습니다.
스타일 단계의 기본 출력물은 두 가지가 있습니다. DOM 트리에서 CSS 계단식 알고리즘을 실행한 결과가 포함된 ComputedStyle
와 레이아웃 단계의 작업 순서를 설정하는 LayoutObjects
트리입니다. 개념적으로 계단식 알고리즘 실행은 레이아웃 트리를 생성하기 전에 엄격하게 실행되어야 하지만 이전에는 이 두 작업이 교차되었습니다. Project Squad는 이 두 가지를 구분된 순차 단계로 분할하는 데 성공했습니다.
이전에는 ComputedStyle
가 스타일 재계산 중에 최종 값을 가져오지 않는 경우가 있었습니다. 후반 파이프라인 단계에서 ComputedStyle
가 업데이트되는 몇 가지 상황이 있었습니다. Project Squad는 이러한 코드 경로를 리팩터링하여 스타일 단계 후에는 ComputedStyle
가 수정되지 않도록 했습니다.
LayoutNG: 레이아웃 단계 파이프라인 처리
RenderingNG의 초석 중 하나인 이 기념비적인 프로젝트는 레이아웃 렌더링 단계를 완전히 재작성한 것입니다. 여기서는 전체 프로젝트를 다루지는 않지만 전체 BlinkNG 프로젝트에서 주목할 만한 몇 가지 측면이 있습니다.
- 이전에는 레이아웃 단계에서 스타일 단계에서 만든
LayoutObject
트리를 수신하고 크기 및 위치 정보로 트리에 주석을 추가했습니다. 따라서 입력과 출력이 명확하게 구분되지 않았습니다. LayoutNG에서는 레이아웃의 기본 읽기 전용 출력이며 후속 렌더링 단계의 기본 입력으로 사용되는 프래그먼트 트리를 도입했습니다. - LayoutNG는 컨테이너 속성을 레이아웃에 도입했습니다. 이제는 지정된
LayoutObject
의 크기와 위치를 계산할 때 더 이상 해당 객체에 루팅된 하위 트리 외부를 확인하지 않습니다. 지정된 객체의 레이아웃을 업데이트하는 데 필요한 모든 정보는 사전에 계산되어 알고리즘에 읽기 전용 입력으로 제공됩니다. - 이전에는 레이아웃 알고리즘이 제대로 작동하지 않는 특이한 경우가 있었습니다. 알고리즘의 결과는 이전에 가장 최근에 업데이트된 레이아웃에 따라 달라졌습니다. LayoutNG는 이러한 사례를 제거했습니다.
사전 페인트 단계
이전에는 공식적인 페인트 전 렌더링 단계가 없었으며, 레이아웃 후 작업의 모음만 있었습니다. pre-paint 단계는 레이아웃이 완료된 후 레이아웃 트리의 체계적인 탐색으로 가장 잘 구현할 수 있는 몇 가지 관련 함수가 있다는 인식에서 비롯되었습니다. 가장 중요한 것은 다음과 같습니다.
- 페인트 무효화 문제: 불완전한 정보가 있는 경우 레이아웃 과정에서 페인트 무효화를 올바르게 실행하기가 매우 어렵습니다. 두 가지 고유한 프로세스로 분할하면 훨씬 더 쉽게 올바르게 처리할 수 있으며 매우 효율적일 수 있습니다. 스타일 및 레이아웃 중에 콘텐츠를 간단한 불리언 플래그로 '페인트 무효화가 필요할 수 있음'으로 표시할 수 있습니다. 사전 페인트 트리 워크 중에 이러한 플래그를 확인하고 필요한 경우 무효화를 실행합니다.
- 페인트 속성 트리 생성: 나중에 자세히 설명하는 프로세스입니다.
- 픽셀 스냅 페인트 위치 계산 및 기록: 기록된 결과는 중복 계산 없이 페인트 단계와 이러한 결과가 필요한 모든 다운스트림 코드에서 사용할 수 있습니다.
속성 트리: 일관된 도형
속성 트리는 스크롤의 복잡성을 처리하기 위해 RenderingNG 초기에 도입되었습니다. 스크롤은 웹에서 다른 모든 종류의 시각 효과와 다른 구조를 갖습니다. 속성 트리 이전에는 Chromium의 컴포저이터가 단일 '레이어' 계층 구조를 사용하여 합성된 콘텐츠의 기하학적 관계를 나타냈지만, position:fixed와 같은 기능의 전체 복잡성이 명확해지면서 이 방법은 빠르게 사라졌습니다. 레이어 계층 구조는 레이어의 '스크롤 상위 요소' 또는 '클립 상위 요소'를 나타내는 추가 비로컬 포인터를 생성했으며, 얼마 지나지 않아 코드를 이해하기가 매우 어려워졌습니다.
속성 트리는 콘텐츠의 오버플로 스크롤 및 클립 측면을 다른 모든 시각적 효과와 별도로 표현하여 이 문제를 해결했습니다. 이를 통해 웹사이트의 실제 시각적 및 스크롤 구조를 올바르게 모델링할 수 있었습니다. 다음으로, 컴포지션된 레이어의 화면 공간 변환이나 스크롤되는 레이어와 스크롤되지 않는 레이어를 결정하는 것과 같은 속성 트리 위에 알고리즘을 구현하기만 하면 되었습니다.
실제로 코드에서 유사한 기하학적 질문이 제기되는 다른 장소가 많다는 것을 곧 알게 되었습니다. 주요 데이터 구조 게시물에 더 완전한 목록이 있습니다. 그중 몇몇은 컴포저 코드가 실행하는 것과 동일한 작업을 중복으로 구현했습니다. 모두 다른 버그 하위 집합이 있었으며, 실제 웹사이트 구조를 제대로 모델링한 코드는 없었습니다. 그러면 해결 방법이 명확해집니다. 모든 도형 알고리즘을 한곳으로 중앙 집중화하고 이를 사용하도록 모든 코드를 리팩터링합니다.
이러한 알고리즘은 모두 속성 트리에 종속되므로 속성 트리는 RenderingNG의 핵심 데이터 구조(즉, 파이프라인 전체에서 사용되는 데이터 구조)입니다. 따라서 중앙 집중식 도형 코드라는 이 목표를 달성하기 위해 파이프라인의 훨씬 앞선 단계인 사전 페인트에서 속성 트리 개념을 도입하고, 이제 속성 트리에 종속된 모든 API를 실행하기 전에 사전 페인트를 실행해야 하도록 변경해야 했습니다.
이 스토리는 BlinkNG 리팩터링 패턴의 또 다른 측면입니다. 주요 계산을 식별하고 중복을 방지하도록 리팩터링하고, 이를 제공하는 데이터 구조를 만드는 잘 정의된 파이프라인 단계를 만듭니다. 필요한 모든 정보를 사용할 수 있는 정확한 시점에 속성 트리를 계산하고 나중에 렌더링 단계가 실행되는 동안 속성 트리가 변경되지 않도록 합니다.
페인트 후 합성: 페인트 및 합성 파이프라인
레이어화는 자체 조합된 레이어 (GPU 텍스처를 나타냄)에 들어가는 DOM 콘텐츠를 파악하는 프로세스입니다. RenderingNG 이전에는 페인트 후에 레이어화가 실행되지 않고 페인트 전에 실행되었습니다 (현재 파이프라인은 여기를 참고하세요. 순서 변경에 유의하세요). 먼저 DOM의 어떤 부분이 어떤 합성된 레이어에 들어가는지 결정한 다음에야 이러한 텍스처의 디스플레이 목록을 그립니다. 당연히 이러한 결정은 애니메이션 또는 스크롤이 적용되거나 3D 변환이 적용된 DOM 요소, 그리고 어떤 요소가 어떤 요소 위에 그려지는지와 같은 요인에 따라 달라집니다.
이는 코드에 순환 종속 항목이 있어야 했으므로 심각한 문제가 발생했습니다. 이는 렌더링 파이프라인에 큰 문제입니다. 예를 통해 그 이유를 알아보겠습니다. 페인트를 무효화해야 한다고 가정해 보겠습니다. 즉, 디스플레이 목록을 다시 그린 다음 다시 래스터링해야 합니다. 무효화 필요성은 DOM 변경사항 또는 변경된 스타일이나 레이아웃에서 발생할 수 있습니다. 하지만 실제로 변경된 부분만 무효화해야 합니다. 즉, 영향을 받은 합성 레이어를 찾은 다음 해당 레이어의 디스플레이 목록 일부 또는 전부를 무효화해야 했습니다.
즉, 무효화는 DOM, 스타일, 레이아웃, 이전 레이어 결정 (이전: 이전에 렌더링된 프레임의 의미)에 종속되었습니다. 하지만 현재의 계층화는 이러한 모든 요소에 의존합니다. 또한 모든 레이어링 데이터의 사본이 두 개가 아니었기 때문에 과거와 미래의 레이어링 결정 간에 차이를 파악하기 어려웠습니다. 결국 순환 논리가 포함된 코드가 많이 생겼습니다. 이로 인해 논리적이지 않거나 잘못된 코드가 생성되거나, 주의하지 않으면 비정상 종료나 보안 문제가 발생하기도 했습니다.
이 상황을 처리하기 위해 초기에 DisableCompositingQueryAsserts
객체의 개념을 도입했습니다. 대부분의 경우 코드가 이전 레이어 결정을 쿼리하려고 하면 어설션 실패가 발생하고 디버그 모드인 경우 브라우저가 비정상 종료됩니다. 이렇게 하면 새로운 버그가 도입되지 않습니다. 그리고 코드가 이전 레이어링 결정을 합법적으로 쿼리해야 하는 각 경우에 DisableCompositingQueryAsserts
객체를 할당하여 이를 허용하는 코드를 넣었습니다.
계획은 시간이 지남에 따라 모든 호출 사이트 DisableCompositingQueryAssert
객체를 삭제한 다음 코드를 안전하고 올바르게 선언하는 것이었습니다. 하지만 페인트 전에 레이어화가 발생하는 한 많은 호출을 삭제할 수 없다는 사실을 발견했습니다. (최근에야 이 문제를 해결할 수 있었습니다.) 이것이 Composite After Paint 프로젝트의 첫 번째 발견된 이유였습니다. 작업에 대해 잘 정의된 파이프라인 단계가 있더라도 파이프라인의 잘못된 위치에 있으면 결국 중단된다는 사실을 알게 되었습니다.
Composite After Paint 프로젝트의 두 번째 이유는 기본 컴포지션 버그였습니다. 이 버그를 설명하는 한 가지 방법은 DOM 요소가 웹페이지 콘텐츠의 효율적이거나 완전한 레이어링 스킴을 1:1로 잘 표현하지 못한다는 것입니다. 합성은 페인트 전에 이루어지므로 디스플레이 목록이나 속성 트리가 아닌 DOM 요소에 본질적으로 의존했습니다. 이는 속성 트리를 도입한 이유와 매우 유사하며, 속성 트리와 마찬가지로 올바른 파이프라인 단계를 파악하고 적절한 시점에 실행하고 올바른 키 데이터 구조를 제공하면 솔루션이 직접 도출됩니다. 또한 속성 트리와 마찬가지로 페인트 단계가 완료되면 후속 파이프라인 단계에서 출력이 변경되지 않도록 보장할 수 있었습니다.
이점
보시다시피 잘 정의된 렌더링 파이프라인은 장기적으로 큰 이점을 제공합니다. 생각보다 더 많은 기능이 있습니다.
- 안정성 크게 개선: 이 문제는 매우 간단합니다. 잘 정의되고 이해하기 쉬운 인터페이스가 있는 더 깔끔한 코드는 이해, 작성, 테스트하기가 더 쉽습니다. 이렇게 하면 더 안정적입니다. 또한 비정상 종료와 use-after-free 버그가 줄어 코드가 더 안전하고 안정적으로 됩니다.
- 확장된 테스트 적용 범위: BlinkNG 과정에서 테스트 모음에 많은 새로운 테스트가 추가되었습니다. 여기에는 내부 요소를 집중적으로 확인하는 단위 테스트, 수정된 오류가 다시 도입되지 않도록 하는 회귀 테스트, 모든 브라우저에서 웹 표준 준수를 측정하는 데 사용하는 공개적이고 공동으로 유지 관리되는 웹 플랫폼 테스트 모음에 추가된 많은 항목이 포함됩니다.
- 확장하기 쉬움: 시스템이 명확한 구성요소로 분류되면 현재 구성요소를 진행하기 위해 다른 구성요소를 세부적으로 이해할 필요가 없습니다. 이렇게 하면 전문 지식이 없어도 누구나 렌더링 코드에 가치를 더할 수 있으며 전체 시스템의 동작에 관해 더 쉽게 추론할 수 있습니다.
- 성능: 스파게티 코드로 작성된 알고리즘을 최적화하는 것만으로도 충분히 어렵지만, 이러한 파이프라인 없이는 범용 스레드 스크롤 및 애니메이션이나 사이트 격리를 위한 프로세스 및 스레드와 같은 더 큰 작업을 달성하는 것은 거의 불가능합니다. 병렬 처리는 성능을 크게 개선하는 데 도움이 되지만 매우 복잡합니다.
- 생성 및 격리: BlinkNG를 통해 파이프라인을 새롭고 독특한 방식으로 실행할 수 있는 몇 가지 새로운 기능이 있습니다. 예를 들어 예산이 만료될 때까지만 렌더링 파이프라인을 실행하려면 어떻게 해야 하나요? 또는 현재 사용자와 관련이 없는 것으로 알려진 하위 트리의 렌더링을 건너뛰나요? content-visibility CSS 속성은 이를 사용 설정합니다. 구성요소의 스타일을 레이아웃에 종속되도록 하려면 어떻게 해야 하나요? 이것이 컨테이너 쿼리입니다.
우수사례: 컨테이너 쿼리
컨테이너 쿼리는 많은 기대를 모으고 있는 향후 웹 플랫폼 기능입니다 (수년 동안 CSS 개발자가 가장 많이 요청한 기능이었습니다). 그렇게 좋은 기능인데 아직 없는 이유는 무엇인가요? 컨테이너 쿼리를 구현하려면 스타일과 레이아웃 코드 간의 관계를 매우 신중하게 이해하고 제어해야 하기 때문입니다. 좀 더 자세히 살펴보겠습니다.
컨테이너 쿼리를 사용하면 요소에 적용되는 스타일이 조상 요소의 레이아웃된 크기에 종속될 수 있습니다. 레이아웃된 크기는 레이아웃 중에 계산되므로 레이아웃 후에 스타일 재계산을 실행해야 합니다. 하지만 스타일 재계산은 레이아웃 전에 실행됩니다. 이 닭과 달걀 역설이 BlinkNG 이전에 컨테이너 쿼리를 구현할 수 없었던 주된 이유입니다.
이 문제를 어떻게 해결할 수 있나요? 이는 역방향 파이프라인 종속 항목, 즉 Composite After Paint와 같은 프로젝트에서 해결한 것과 동일한 문제인가요? 더 나쁜 상황은 새 스타일이 조상의 크기를 변경하는 경우입니다. 이렇게 하면 무한 루프가 발생하지 않나요?
원칙적으로 순환 종속 항목은 contain CSS 속성을 사용하여 해결할 수 있습니다. 이 속성을 사용하면 요소 외부의 렌더링이 해당 요소의 하위 트리 내 렌더링에 종속되지 않도록 할 수 있습니다. 즉, 컨테이너 쿼리에는 포함이 필요하므로 컨테이너에서 적용한 새 스타일은 컨테이너의 크기에 영향을 줄 수 없습니다.
하지만 실제로는 이 정도만으로는 충분하지 않았으며 크기 제한보다 약한 유형의 제한을 도입해야 했습니다. 이는 컨테이너 쿼리 컨테이너가 인라인 크기에 따라 한 방향 (일반적으로 블록)으로만 크기를 조절할 수 있기를 원하는 경우가 많기 때문입니다. 따라서 인라인 크기 제한 개념이 추가되었습니다. 하지만 이 섹션의 매우 긴 메모에서 알 수 있듯이 인라인 크기 제한이 가능한지 오랫동안 명확하지 않았습니다.
추상적인 사양 언어로 포함을 설명하는 것과 올바르게 구현하는 것은 전혀 다른 문제입니다. BlinkNG의 목표 중 하나는 렌더링의 기본 로직을 구성하는 트리 워크에 컨테이너링 원칙을 적용하는 것이었습니다. 즉, 하위 트리를 탐색할 때 하위 트리 외부에서 정보가 필요하지 않아야 합니다. 렌더링 코드가 컨테이닝 원칙을 준수하면 CSS 컨테이닝을 훨씬 더 깔끔하고 쉽게 구현할 수 있습니다.
향후: 기본 스레드 외부 합성 등
여기에 표시된 렌더링 파이프라인은 실제로 현재 RenderingNG 구현보다 약간 앞서 있습니다. 레이어링이 기본 스레드 외부에 있는 것으로 표시되지만 현재는 여전히 기본 스레드에 있습니다. 하지만 이제 Composite After Paint가 출시되고 레이어화가 페인트 후에 이루어지므로 이 작업이 완료되는 것은 시간 문제일 뿐입니다.
이 기능이 중요한 이유와 이 기능이 어디로 이어질 수 있는지 이해하려면 렌더링 엔진의 아키텍처를 좀 더 높은 관점에서 고려해야 합니다. Chromium의 성능을 개선하는 데 가장 오래 지속되는 장애물 중 하나는 렌더러의 기본 스레드가 기본 애플리케이션 로직 (즉, 실행 중인 스크립트)과 대부분의 렌더링을 모두 처리한다는 단순한 사실입니다. 그 결과 기본 스레드가 작업으로 포화되는 경우가 많으며 기본 스레드 혼잡이 전체 브라우저의 병목 현상인 경우가 많습니다.
다행히도 반드시 그런 것은 아닙니다. Chromium 아키텍처의 이 측면은 단일 스레드 실행이 주류 프로그래밍 모델이었던 KHTML 시절로 거슬러 올라갑니다. 멀티코어 프로세서가 소비자용 기기에서 보편화될 때쯤에는 단일 스레드 가정이 Blink (이전의 WebKit)에 완전히 통합되었습니다. 오래 전부터 렌더링 엔진에 더 많은 스레딩을 도입하고 싶었지만 이전 시스템에서는 불가능했습니다. 렌더링 NG의 주요 목표 중 하나는 이 문제를 해결하고 렌더링 작업을 부분적으로 또는 전체적으로 다른 스레드로 이동할 수 있도록 하는 것이었습니다.
이제 BlinkNG가 완성 단계에 접어들면서 이 영역을 탐색하기 시작했습니다. 비차단 커밋은 렌더러의 스레딩 모델을 변경하기 위한 첫 번째 시도입니다. 컴포저이터 커밋 (또는 커밋)은 기본 스레드와 컴포저이터 스레드 간의 동기화 단계입니다. 커밋 중에 컴포저 스레드에서 실행되는 다운스트림 컴포지션 코드에서 사용할 수 있도록 기본 스레드에서 생성된 렌더링 데이터의 사본을 만듭니다. 이 동기화가 진행되는 동안 기본 스레드 실행은 중지되고 복사 코드는 컴포저 스레드에서 실행됩니다. 이는 컴포저 스레드가 렌더링 데이터를 복사하는 동안 기본 스레드가 렌더링 데이터를 수정하지 않도록 하기 위함입니다.
비차단 커밋을 사용하면 기본 스레드가 중지되고 커밋 단계가 종료될 때까지 기다릴 필요가 없습니다. 기본 스레드는 컴포저 스레드에서 커밋이 동시에 실행되는 동안 작업을 계속 실행합니다. 비차단 커밋의 순 효과는 기본 스레드에서 렌더링 작업에 할당되는 시간이 줄어들어 기본 스레드의 정체가 줄고 성능이 개선된다는 것입니다. 이 글을 작성하는 시점 (2022년 3월)에는 비차단 커밋의 작동하는 프로토타입이 있으며, 실적에 미치는 영향을 자세히 분석하기 위해 준비하고 있습니다.
기본 스레드 외부 합성도 준비 중입니다. 이 기능은 레이어링을 기본 스레드 외부로 이동하여 렌더링 엔진을 그림과 일치시키는 것을 목표로 합니다. 비차단 커밋과 마찬가지로 렌더링 워크로드를 줄여 기본 스레드의 정체를 줄입니다. 이와 같은 프로젝트는 Composite After Paint의 아키텍처 개선 없이는 불가능했을 것입니다.
앞으로 더 많은 프로젝트가 준비되어 있습니다. 이제 렌더링 작업 재분배를 실험할 수 있는 기반이 마련되었으며, 앞으로 어떤 결과가 나올지 기대됩니다.