RenderingNG 심층 분석: BlinkNG

스테판 자거
스테판 자거
크리스 해럴슨
크리스 해럴슨

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

BlinkWebKit의 포크로 시작되었습니다. WebKit는 그 자체로 1998년으로 거슬러 올라가는 KHTML의 포크입니다. Chromium에서 가장 오래되고 가장 중요한 코드가 일부 포함되어 있으며 2014년에는 확실히 그 나이를 확인할 수 있게 되었습니다. 그 해, BlinkNG라는 명칭으로 불리는 야심 찬 프로젝트를 시작하여 Blink 코드의 조직과 구조의 오래된 결함을 해결한다는 목표를 세웠습니다. 이 글에서는 BlinkNG 및 관련 프로젝트, 즉 BlinkNG를 만든 이유, 이들이 어떤 성과를 달성했는지, 디자인을 결정하게 된 기본 원칙, 향후 개선 기회를 엿볼 수 있는 기회에 대해 살펴봅니다.

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

NG 이전 렌더링

Blink 내의 렌더링 파이프라인은 항상 개념적으로 여러 단계 (스타일, 레이아웃, 페인트 등)로 분할되었지만 추상화 장벽이 많았습니다. 개략적으로 렌더링과 관련된 데이터는 오래 지속되는 변경 가능한 객체로 구성되었습니다. 이러한 객체는 언제든지 수정될 수 있으며, 연속적인 렌더링 업데이트를 통해 자주 재활용되고 재사용되었습니다. 다음과 같은 간단한 질문에 안정적으로 답변할 수 없었습니다.

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

다음을 포함하여 이에 대한 많은 예가 있습니다.

스타일은 스타일시트를 기반으로 ComputedStyle를 생성하지만, ComputedStyle는 변경할 수 없었습니다. 경우에 따라 파이프라인 후반 단계에서 수정될 수 있었습니다.

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

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

하위 수준에서 데이터 유형 렌더링은 주로 특수 트리 (예: DOM 트리, 스타일 트리, 레이아웃 트리, 페인트 속성 트리)로 구성됩니다. 렌더링 단계는 재귀 트리 워크로 구현됩니다. 트리 워크를 포함하는 것이 이상적입니다. 지정된 트리 노드를 처리할 때 이 노드에 루트된 하위 트리 외부의 정보에 액세스하지 않아야 합니다. 하지만 이는 RenderingNG 전에는 그렇지 않았습니다. 트리는 처리 중인 노드의 상위 요소에서 정보에 자주 액세스했습니다. 이로 인해 시스템이 매우 취약하고 오류가 발생하기 쉽습니다. 나무의 뿌리 밖에는 아무 곳에서나 나무 산책을 시작할 수도 없었습니다.

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

변경사항

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

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

BlinkNG 하위 프로젝트의 전체 목록은 지루하게 읽을 수 있지만 다음은 몇 가지 특별한 결과입니다.

문서 수명 주기

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

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

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

어딜 가도 저수준 렌더링 코드를 보다가 '어떻게 여기까지 왔는지' 궁금해 할 것입니다. 앞서 언급했듯이 렌더링 파이프라인에는 다양한 진입점이 있습니다. 이전에는 재귀 및 재진입 호출 경로가 포함되었으며 파이프라인에 처음부터 시작하지 않고 중간 단계에서 파이프라인에 진입했습니다. BlinkNG 과정에서 이러한 호출 경로를 분석하여 다음 두 가지 기본 시나리오로 모두 줄일 수 있음을 확인했습니다.

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

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

파이프라이닝 스타일, 레이아웃, 사전 페인트

페인트 이전의 렌더링 단계는 다음과 같은 역할을 합니다.

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

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

BlinkNG 이전 문제의 또 다른 좋은 예는 페인트 무효화입니다. 이전에는 페인트에 이르는 모든 렌더링 단계에 페인트 무효화가 흩어졌습니다. 스타일 또는 레이아웃 코드를 수정할 때 페인트 무효화 로직에 어떤 변경이 필요한지 알기 어려웠고 실수를 저지르기 쉬웠으며 잘못하여 무효화 또는 과도한 무효화 버그로 이어지는 경우도 많았습니다. 이전 페인트 무효화 시스템의 복잡성에 대한 자세한 내용은 LayoutNG 전용 시리즈의 도움말을 참고하세요.

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

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

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

이 프로젝트는 스타일 단계의 두 가지 주요 결함을 해결하여 파이프라인을 깔끔하게 처리할 수 없었습니다.

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

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

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

RenderingNG의 초석 중 하나인 이 기념비 프로젝트는 레이아웃 렌더링 단계를 완전히 다시 작성한 것이었습니다. 여기서는 전체 프로젝트를 다루지는 않지만 전체 BlinkNG 프로젝트에 관해 몇 가지 주목할 만한 측면이 있습니다.

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

사전 페인트 단계

이전에는 공식적인 사전 페인트 렌더링 단계가 없었고 레이아웃 후 작업만 하면 되었습니다. 사전 페인트 단계는 레이아웃이 완료된 후 레이아웃 트리의 체계적인 순회로 가장 적합하게 구현할 수 있는 몇 가지 관련 함수가 있다는 사실을 인지하면서 비롯되었습니다. 가장 중요한 점은 다음과 같습니다.

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

속성 트리: 일관된 도형

속성 트리는 웹에서 다른 모든 종류의 시각 효과와 구조가 다른 스크롤의 복잡성을 처리하기 위해 RenderingNG 초기에 도입되었습니다. 속성 트리를 사용하기 전에는 Chromium의 컴포지터가 단일 '레이어' 계층 구조를 사용하여 합성된 콘텐츠의 기하학적 관계를 나타내지만, 이러한 관계는 position:fixed와 같은 지형지물의 전체 복잡성이 명백해지면서 빠르게 없어졌습니다. 레이어 계층 구조에서 레이어의 '스크롤 상위 요소' 또는 '클립 상위 요소'를 나타내는 로컬이 아닌 포인터가 추가로 증가했으며, 머지않아 코드를 이해하기가 매우 어려웠습니다.

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

사실, 우리는 곧 코드에서 유사한 기하학적 질문이 제기되는 다른 위치가 많이 있다는 것을 알아차렸습니다. 주요 데이터 구조 게시물에서 더 완전한 목록을 확인할 수 있습니다. 이들 중 일부는 컴포지터 코드가 하는 것과 동일한 작업을 중복 구현했고, 모두 각기 다른 하위 집합의 버그가 있었으며, 그 중 어느 것도 실제 웹사이트 구조를 적절하게 모델링하지 않았습니다. 그런 다음 해결책이 명확해졌습니다. 모든 기하학 알고리즘을 한 곳에 중앙 집중화하고 모든 코드를 리팩터링하여 사용할 수 있습니다.

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

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

페인트 후 합성: 파이프라이닝 페인트 및 합성

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

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

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

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

우리의 계획은 시간이 지남에 따라 모든 호출 사이트의 DisableCompositingQueryAssert 객체를 삭제한 다음 코드를 안전하고 정확하다고 선언하는 것이었습니다. 하지만 우리가 발견한 사실은 페인트 전에 레이어화가 일어나는 한, 많은 콜을 본질적으로 제거하는 것이 불가능하다는 것입니다. (최근에 삭제되었습니다.) 이것이 페인트 후 합성 프로젝트에서 발견된 첫 번째 이유입니다. 작업을 위한 파이프라인 단계가 잘 정의되어 있더라도 파이프라인의 잘못된 위치에 있으면 결국 문제가 발생하는 것을 알게 되었습니다.

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

이점

앞에서 살펴보았듯이 렌더링 파이프라인을 잘 정의하면 장기적으로 막대한 이점을 얻을 수 있습니다. 이 외에도 다양한 기능이 있습니다.

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

우수사례: 컨테이너 쿼리

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

컨테이너 쿼리를 사용하면 요소에 적용되는 스타일이 상위 항목의 배치된 크기에 종속될 수 있습니다. 배치된 크기는 레이아웃 중에 계산되므로 레이아웃 후에 스타일 재계산을 실행해야 하지만 스타일 재계산은 레이아웃 전에 실행됩니다. BlinkNG 이전에는 컨테이너 쿼리를 구현하지 못했던 이유가 바로 닭과 달걀의 역설입니다.

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

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

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

포함을 추상적인 사양 언어로 설명하는 것과 이것을 올바르게 구현하는 것은 별개의 문제입니다. BlinkNG의 목표 중 하나는 렌더링의 기본 로직을 구성하는 트리 워크에 포함 원칙을 적용하는 것이라는 점을 떠올려 보세요. 하위 트리를 순회할 때는 하위 트리 외부에서 정보를 확인할 필요가 없습니다. (정확히 우연히 발생한 것은 아님) 렌더링 코드가 포함 원칙을 준수하면 CSS 포함을 훨씬 더 깔끔하고 쉽게 구현할 수 있습니다.

향후: 기본 스레드를 사용하지 않는 합성 등

여기에 나와 있는 렌더링 파이프라인은 실제로 현재의 RenderingNG 구현보다 약간 앞서 있습니다. 계층화가 기본 스레드를 벗어난 것으로 표시되지만 현재는 여전히 기본 스레드에 있습니다. 하지만 이제 Composite After Paint(페인트 후 합성)가 출시되고 레이어화가 페인트 이후에 진행되므로 이 작업은 시간 문제일 뿐입니다.

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

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

BlinkNG가 완성되는 시점이므로 이 영역을 탐구하기 시작했습니다. 비차단 커밋은 렌더러의 스레딩 모델을 변경하기 위한 첫 번째 단계입니다. 컴포지터 커밋 (또는 간단히 커밋)은 기본 스레드와 컴포지터 스레드 간의 동기화 단계입니다. 커밋 중에는 기본 스레드에서 생성된 렌더링 데이터의 사본을 만들어 컴포지터 스레드에서 실행되는 다운스트림 합성 코드에서 사용할 수 있도록 합니다. 이 동기화가 진행되는 동안 복사 코드가 컴포지터 스레드에서 실행되는 동안 기본 스레드 실행이 중지됩니다. 이는 컴포지터 스레드에서 기본 스레드가 렌더링 데이터를 복사하는 동안 기본 스레드에서 렌더링 데이터를 수정하지 않도록 하기 위해 수행됩니다.

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

오프메인 스레드 합성레이어화를 기본 스레드에서 작업자 스레드로 이동하여 렌더링 엔진이 그림과 일치하도록 하는 것을 목표로 합니다. 비차단 커밋과 마찬가지로 렌더링 워크로드를 줄여 기본 스레드의 정체를 줄입니다. Composite After Paint(복합 페인트 후 페인트)의 아키텍처 개선이 없었다면 이러한 프로젝트는 불가능했을 것입니다.

그리고 파이프라인에 더 많은 프로젝트가 있습니다. 마침내 렌더링 작업을 재배포하는 실험을 할 수 있는 기반을 마련했으며, 무엇이 가능한지 무척 기대가 됩니다.