RenderingNG 심층 분석: LayoutNG

Ian Kilpatrick
Ian Kilpatrick
Koji Ishi
Koji Ishi

저는 이안 킬패트릭입니다 Koji Ishii와 함께 Blink 레이아웃팀의 엔지니어링 리드입니다. Blink팀에서 일하기 전에 저는 Google이 '프런트엔드 엔지니어' 역할을 맡기 전에 프런트엔드 엔지니어였습니다. Google Docs, Drive, Gmail 내에서 기능을 구축하는 것입니다 그 역할을 맡았던 지 약 5년 후에 저는 큰 도박을 맡았고 Blink 팀으로 전환했습니다. 효과적으로 C++를 배우기 때문에 엄청나게 복잡한 Blink 코드베이스를 확대하려고 시도했습니다 지금도 제가 이해할 수 있는 것은 일부분에 불과합니다. 이 기간 동안 시간을 내주셔서 감사합니다. 많은 '프런트엔드 엔지니어 복구'가 이루어지고 있다는 사실에 위안을 받았습니다. "브라우저 엔지니어"가 되어 말씀드리고 싶었어요.

Blink 팀에서 이전 경험은 개인적으로 큰 도움이 되었습니다. 프런트엔드 엔지니어로서 저는 끊임없이 브라우저 불일치가 발생했습니다. 성능 문제, 렌더링 버그, 기능 누락 등이 있습니다. LayoutNG는 Blink의 레이아웃 시스템 내에서 이러한 문제를 체계적으로 해결할 수 있는 기회였습니다. 이는 여러 엔지니어가 많은 노력을 기울였습니다.

이 게시물에서는 이와 같은 대규모 아키텍처 변경이 다양한 유형의 버그와 성능 문제를 어떻게 줄이고 완화할 수 있는지 설명합니다.

레이아웃 엔진 아키텍처 30,000피트 뷰

이전에는 Blink의 레이아웃 트리를 '변경 가능한 트리'라고 했습니다.

다음 텍스트에 설명된 대로 트리를 표시합니다.

레이아웃 트리의 각 객체에는 입력 정보, 예를 들어 상위 요소가 부여한 사용 가능한 크기 모든 부동 소수점 수의 위치, 출력 정보 예를 들어 객체의 최종 너비와 높이 또는 객체의 x 및 y 위치일 수 있습니다.

이러한 객체는 렌더 사이에 보관되었습니다. 스타일이 변경되면 해당 객체를 더티로 표시했고 마찬가지로 트리에 있는 모든 상위 요소를 표시했습니다. 렌더링 파이프라인의 레이아웃 단계가 실행되었을 때 그런 다음 트리를 정리하고 더러운 객체를 탐색한 다음 레이아웃을 실행하여 깨끗한 상태로 만듭니다.

이러한 아키텍처가 많은 종류의 문제를 초래한다는 것을 발견했습니다. 아래에서 설명하겠습니다. 하지만 먼저 한 걸음 물러서서 레이아웃의 입력과 출력이 무엇인지 생각해 봅시다.

이 트리의 노드에서 레이아웃을 실행할 때는 개념적으로 '스타일 + DOM'을 사용합니다. 및 상위 레이아웃 시스템의 모든 상위 제약 조건 (그리드, 블록 또는 탄력) 레이아웃 제약 조건 알고리즘을 실행하고 결과를 생성합니다.

앞에서 설명한 개념적 모델입니다.

새로운 아키텍처는 이러한 개념 모델을 형식화합니다. 레이아웃 트리는 여전히 있지만 주로 레이아웃의 입력과 출력을 유지하는 데 사용합니다. 출력을 위해 프래그먼트 트리라는 완전히 새로운 변경 불가능한 객체를 생성합니다.

프래그먼트 트리

나는 변경 불가능한 프래그먼트 트리와 증분 레이아웃을 위해 이전 트리의 많은 부분을 재사용하도록 설계된 방법을 설명합니다.

또한 해당 프래그먼트를 생성한 상위 제약 조건 객체를 저장합니다. 이것을 캐시 키로 사용합니다. 이에 대해서는 아래에서 자세히 설명합니다.

인라인 (텍스트) 레이아웃 알고리즘도 새로운 불변 아키텍처와 일치하도록 재작성됩니다. Kubernetes는 변경할 수 없는 플랫 목록 표현 더 빠른 재배치를 위한 단락 수준 캐싱 기능도 제공합니다. 단락당 도형을 적용하여 요소와 단어 전반에 글꼴 기능을 적용 ICU를 사용하는 새로운 유니코드 양방향 알고리즘, 다양한 정확성 수정 등이 있습니다.

레이아웃 버그의 유형

레이아웃 버그는 크게 네 가지 카테고리로 나눌 수 있습니다. 각각 다른 근본 원인을 가지고 있습니다

정확성

렌더링 시스템의 버그라고 할 때는 일반적으로 정확성, 예: "브라우저 A에는 X 동작이 있지만, 브라우저 B에는 Y 동작이 있습니다.", 또는 '브라우저 A와 B 모두 손상되었습니다.'와 같은 메시지가 표시될 수 있습니다. 이전에는 이것이 많은 시간을 보낸 것이었습니다. 그 과정에서 시스템과 싸웠습니다. 일반적인 장애 모드는 하나의 버그에 대해 고도로 맞춤화된 수정을 적용하는 것이었습니다. 그러나 몇 주 후에 시스템의 다른 부분에서 회귀가 발생한 것을 발견했습니다.

이전 게시물에서 설명한 것처럼 이는 시스템이 매우 불안정하다는 신호입니다. 특히 레이아웃의 경우 클래스 간에 명확한 계약이 없었습니다. 브라우저 엔지니어가 상태에 의존하게 되어 시스템의 다른 부분에서 일부 값을 잘못 해석할 수 있습니다.

한 예로 1년이 넘는 기간에 걸쳐 약 10개의 버그로 구성된 연쇄적 버그가 발생한 적이 있었습니다. 플렉스 레이아웃과 관련이 있습니다. 각 수정사항은 시스템의 일부에서 정확성 또는 성능 문제를 야기했습니다. 또 다른 버그로 이어집니다.

이제 LayoutNG가 레이아웃 시스템의 모든 구성요소 간의 계약을 명확하게 정의하므로 훨씬 더 자신 있게 변경사항을 적용할 수 있음을 알게 되었습니다. 또한 우수한 웹 플랫폼 테스트 (WPT) 프로젝트가 매우 유용합니다. 이를 통해 여러 당사자가 공통의 웹 테스트 모음에 기여할 수 있습니다.

오늘은 안정적인 채널에서 실제 회귀를 공개하면 일반적으로 WPT 저장소에 관련된 테스트가 없습니다. 구성요소 계약에 대한 오해로 인한 것이 아닙니다. 또한 버그 수정 정책의 일환으로 Google에서는 항상 새로운 WPT 테스트, 이렇게 하면 브라우저가 같은 실수를 다시 하지 않도록 할 수 있습니다.

무효화 부족

브라우저 창의 크기를 조절하거나 CSS 속성을 마법처럼 바꾸면 버그가 사라지는 알 수 없는 버그가 발생한 적이 있다면 무효화와 무관한 문제에 직면하게 됩니다. 변경 가능한 트리의 일부가 사실상 깨끗한 것으로 간주되었습니다. 상위 제약조건의 일부 변경으로 인해 올바른 출력을 나타내지 않았습니다.

이것은 2-패스를 사용하는 경우 (최종 레이아웃 상태를 결정하기 위해 레이아웃 트리를 두 번 이동) 아래에 설명된 레이아웃 모드입니다. 이전 코드는 다음과 같았습니다.

if (/* some very complicated statement */) {
  child->ForceLayout();
}

이 유형의 버그는 일반적으로 다음과 같이 수정합니다.

if (/* some very complicated statement */ ||
    /* another very complicated statement */) {
  child->ForceLayout();
}

이러한 유형의 문제를 해결하면 일반적으로 심각한 성능 회귀가 발생합니다. (아래의 과도한 무효화 참조) 정답을 맞히는 데 매우 섬세했습니다.

(위에서 설명한 대로) 현재는 상위 요소 레이아웃에서 하위 요소로의 모든 입력을 설명하는 변경 불가능한 상위 제약 조건 객체가 있습니다. 그 결과로 생성되는 불변 프래그먼트와 함께 이것을 저장합니다. 이로 인해 하위 요소에서 또 다른 레이아웃 패스를 실행해야 하는지 확인하기 위해 이러한 두 입력을 차이하는 중앙 집중식 위치가 있습니다. 이 디핑 로직은 복잡하지만 잘 포함되어 있습니다. 이러한 무효화 부족 문제를 디버깅하면 일반적으로 두 입력을 수동으로 검사하게 됩니다. 또 다른 레이아웃 패스가 필요하도록 입력의 변경 사항을 결정할 수 있습니다.

이 디핑 코드의 수정은 일반적으로 간단하지만 쉽게 단위 테스트할 수 있습니다.

<ph type="x-smartling-placeholder">
</ph> 고정 너비 및 백분율 너비 이미지를 비교합니다. <ph type="x-smartling-placeholder">
</ph> 고정된 너비/높이 요소는 지정된 사용 가능한 크기가 증가해도 상관없지만 백분율 기반 너비/높이는 증가시킵니다. available-sizeParent Constraints 객체에 표시되며 디핑 알고리즘의 일부로 이 최적화를 수행합니다.

위 예의 디핑 코드는 다음과 같습니다.

if (width.IsPercent()) {
  if (old_constraints.WidthPercentageSize() 
    != new_constraints.WidthPercentageSize())
   return kNeedsLayout;
}
if (height.IsPercent()) {
  if (old_constraints.HeightPercentageSize() 
    != new_constraints.HeightPercentageSize())
   return kNeedsLayout;
}

이력 현상

이 버그 클래스는 과소 무효화와 유사합니다. 기본적으로 이전 시스템에서는 레이아웃이 멱등성을 갖는지, 즉 동일한 입력으로 레이아웃을 재실행하면 동일한 출력이 생성됩니다.

아래 예에서는 두 값 간에 CSS 속성을 전환합니다. 하지만 그 결과 있습니다.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
</ph> 동영상 및 데모에서는 Chrome 92 이하의 이력 버그를 보여줍니다. 이 문제는 Chrome 93에서 해결되었습니다.

이전의 변경 가능한 트리에서는 이와 같은 버그를 도입하는 일은 엄청나게 쉬웠습니다. 코드가 잘못된 시간 또는 단계에서 객체의 크기나 위치를 읽는 실수를 범한 경우 (예: 이전 크기나 위치를 '지우기'하지 않았기 때문에) 즉시 미세한 이력 현상 버그가 추가됩니다. 대부분의 테스트가 단일 레이아웃 및 렌더링에 중점을 두므로 이러한 버그는 일반적으로 테스트에서 나타나지 않습니다. 더욱 우려스러운 점은 일부 레이아웃 모드가 올바르게 작동하게 하려면 이러한 히스테리시스가 필요하다는 사실이었습니다. 최적화를 실행하여 레이아웃 패스를 삭제하는 버그가 있었습니다. '버그'를 올바른 출력을 얻기 위해 두 번의 패스가 필요했기 때문입니다.

<ph type="x-smartling-placeholder">
</ph> 앞의 텍스트에서 설명한 문제를 보여주는 트리 <ph type="x-smartling-placeholder">
</ph> 이전 레이아웃 결과 정보에 따라 비멱등적 레이아웃이 생깁니다.

LayoutNG를 사용하면 명시적인 입력 및 출력 데이터 구조가 있으므로 이전 상태에 액세스하는 것이 허용되지 않는 경우 레이아웃 시스템에서 이 버그 클래스를 광범위하게 완화했습니다.

과도한 무효화 및 성능

이는 무효화된 버그의 클래스와 정반대입니다. 무효화 부족 버그를 수정할 때 성능 급락을 일으키는 경우가 많습니다.

성능보다는 정확성을 중요시하는 어려운 선택을 해야 하는 경우가 많았습니다. 다음 섹션에서는 이러한 유형의 성능 문제를 완화한 방법을 자세히 살펴보겠습니다.

투 패스 레이아웃과 성능 급의 증가

플렉스 및 그리드 레이아웃은 웹 레이아웃의 표현력 변화를 나타냅니다. 그러나 이러한 알고리즘은 이전의 블록 레이아웃 알고리즘과 근본적으로 달랐습니다.

블록 레이아웃 (거의 모든 경우)은 엔진이 모든 하위 요소에서 정확히 한 번만 레이아웃을 실행하면 됩니다. 이는 성능에는 좋지만 웹 개발자가 원하는 것만큼 표현력이 좋지는 않습니다.

예를 들어 모든 하위 요소의 크기를 가장 큰 하위 요소의 크기로 확장해야 하는 경우가 많습니다. 이를 지원하기 위해 상위 요소 레이아웃 (가변 또는 그리드) 측정 패스를 실행하여 각 하위 요소의 크기를 결정합니다. 레이아웃 패스를 사용하여 모든 하위 요소를 이 크기로 늘립니다. 이 동작은 플렉스 및 그리드 레이아웃의 기본값입니다.

두 개의 상자 세트로, 첫 번째는 측정 패스에서 상자의 고유 크기를 표시하고 두 번째는 레이아웃에서 모두 동일한 높이를 보여줍니다.

이러한 두 패스 레이아웃은 처음에는 성능 측면에서 수용 가능했지만 사람들이 일반적으로 깊숙이 쌓여 있지 않기 때문입니다. 하지만 더 복잡한 콘텐츠가 나오면서 심각한 성능 문제가 나타나기 시작했습니다. 측정 단계의 결과를 캐시하지 않으면 레이아웃 트리는 측정 상태와 최종 레이아웃 상태 사이를 스래싱합니다.

<ph type="x-smartling-placeholder">
</ph> 캡션에 설명된 1, 2, 3 패스 레이아웃입니다. <ph type="x-smartling-placeholder">
</ph> 위 이미지에는 세 개의 <div> 요소가 있습니다. 블록 레이아웃과 같은 단순한 원 패스 레이아웃은 3개의 레이아웃 노드(복잡성 O(n))를 방문합니다. 그러나 두 패스 레이아웃 (예: Flex 또는 그리드)의 경우 이로 인해 이 예에서 O(2n) 방문의 복잡성이 발생할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> 레이아웃 시간의 기하급수적 증가를 보여주는 그래프 <ph type="x-smartling-placeholder">
</ph> 이 이미지와 데모는 그리드 레이아웃이 있는 지수 레이아웃을 보여줍니다. 그리드를 새로운 아키텍처로 이동한 결과 Chrome 93에서 이 문제가 해결되었습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

이전에는 이러한 유형의 성능 저하를 해결하기 위해 플렉스 및 그리드 레이아웃에 매우 구체적인 캐시를 추가하려고 했습니다. 이 방법은 효과가 있었습니다 (Flex를 사용해 본 결과 지금까지 아주 많이 접어들어 왔음). 무효화 버그와 계속 싸우고 있었습니다.

LayoutNG를 사용하면 레이아웃의 입력 및 출력 모두를 위한 명시적 데이터 구조를 생성할 수 있습니다. 무엇보다 측정 및 레이아웃 패스의 캐시를 빌드했습니다. 이로 인해 복잡도가 O(n)으로 되돌아갑니다. 웹 개발자들이 예측 가능한 선형적인 성능을 발휘할 수 있습니다. 레이아웃이 3 패스 레이아웃을 실행하는 경우 해당 패스도 캐시합니다. 이를 통해 향후 RenderingNG가 근본적으로 어떻게 구현되는지 보여주는 예를 통해 고급 레이아웃 모드를 안전하게 도입할 수 있는 기회가 전반적으로 확장성을 활용합니다. 경우에 따라 그리드 레이아웃에 3 패스 레이아웃이 필요할 수 있지만 현재로서는 매우 드뭅니다.

개발자가 특히 레이아웃에서 성능 문제가 발생하면 일반적으로 파이프라인 레이아웃 단계의 원시 처리량이 아니라 지수 레이아웃 시간 버그 때문입니다. 약간의 증분 변경 (하나의 요소가 단일 CSS 속성을 변경)으로 인해 50~100ms의 레이아웃이 발생하는 경우 지수 레이아웃 버그일 가능성이 높습니다.

요약

레이아웃은 매우 복잡한 영역이지만 인라인 레이아웃 최적화와 같은 흥미로운 세부 사항은 다루지 않았습니다. (전체 인라인 및 텍스트 하위 시스템이 실제로 작동하는 방식) 여기서 이야기한 개념조차도 실제로 겉핥기 일 뿐입니다. 자세히 설명하겠습니다 그러나 시스템의 아키텍처를 체계적으로 개선하면 장기적으로 얼마나 큰 이익을 얻을 수 있는지 보여주었길 바랍니다.

그렇긴 하지만 앞으로 할 일이 많다는 것을 알고 있습니다. Google은 성능 및 정확성 등 다양한 문제를 해결하기 위해 노력하고 있습니다. CSS에 새로운 레이아웃 기능이 추가될 예정이니 기대해 주세요 Google은 LayoutNG의 아키텍처 덕분에 이러한 문제를 안전하고 해결할 수 있다고 믿습니다.

Una Kravets의 이미지 1개 (you know what!).