1. 소개
이 절은 비규범적입니다.
웹페이지에서 DOM 요소의 이동은 사용자의 경험을 저해하며, 오늘날 웹에서 자주 발생합니다. 이러한 이동은 종종 콘텐츠가 비동기적으로 로드되면서 페이지의 다른 요소들을 밀어내기 때문에 일어납니다.
Layout Instability API는 사용자 세션의 각 애니메이션 프레임마다 (“layout shift”)라는 값을 보고하여 이러한 불안정한 페이지를 식별합니다. 이 명세는 사용자 에이전트가 레이아웃 이동 값을 계산하는 방법을 제안합니다.
레이아웃 이동 값은 특정 시점의 레이아웃 불안정성의 심각도와 대체로 대응됩니다. 이를 산출하는 방법은 불안정성의 영향을 받은 영역의 면적과 페이지 요소가 이동한 거리를 모두 고려합니다.
이 명세에서 노출하는 값들은 여러 이유로 “레이아웃 변경 관측기”로 사용되기 위한 것이 아닙니다. 첫째, 값들이 PerformanceObserver와 연결되므로 사용자 에이전트는 웹사이트 성능에 영향을 주지 않기 위해 콜백 디스패치를 게으르게(lazily) 할 수 있습니다. 둘째, 매우 작은 레이아웃 이동은 사용자 에이전트가 무시할 수 있습니다. 따라서 이 API에 의존하여 웹사이트의 사용자 가시 동작에 영향을 주는 JavaScript를 실행하는 것은 권장되지 않습니다.
1.1. 누적 레이아웃 이동(CLS)
이 절은 비규범적입니다.
레이아웃 이동 값은 단일 시점을 나타내지만, 사용자가 페이지에 머무르는 기간 전체의 불안정성을 나타내는 값을 갖는 것도 유용합니다.
이를 위해 사용자 에이전트 또는 개발자가 이런 값을 계산할 수 있도록 두 가지 값을 제안합니다. (이 정의들은 비규범적이며 API에서 노출되지 않습니다.)
-
문서 누적 레이아웃 이동(DCLS) 점수는, 단일 브라우징 컨텍스트 내에서 보고된 모든 레이아웃 이동 값의 합입니다. (DCLS 점수는 하위 브라우징 컨텍스트 내의 불안정성은 포함하지 않습니다.)
-
누적 레이아웃 이동(CLS) 점수는, 최상위 브라우징 컨텍스트 내에서 보고된 모든 레이아웃 이동 값의 합과, 하위 브라우징 컨텍스트 내부에서 보고된 각 레이아웃 이동 값의 일부(각 서브프레임 가중치 계수)를 더한 값입니다.
-
서브프레임 가중치 계수는 자식 브라우징 컨텍스트의 레이아웃 이동 값에 대해, 최상위 뷰포트 중 자식 브라우징 컨텍스트의 뷰포트가 차지하는 비율입니다.
누적 레이아웃 이동 점수는 페이지의 생애 동안의 레이아웃 불안정성 심각도와 대체로 대응됩니다.
개발자는 옵저버에 값이 보고될 때마다 합산하고 visibilitychange 이벤트 시점의 “최종” 값으로 DCLS 또는 CLS 점수를 이 API로 계산할 수 있습니다.
이 전략은 사용 예에서 보여집니다.
1.2. 원인 소스 표시
이 절은 비규범적입니다.
레이아웃 이동 값 외에도, API는 애니메이션 프레임별로 레이아웃 이동에 가장 크게 기여한 최대 5개의 DOM 요소 샘플을 보고합니다. sources 값은 영향 영역 기준 내림차순으로 정렬되어, 첫 번째 요소가 해당 이동에 가장 크게 기여한 요소입니다.
실제 불안정성의 “근본 원인”은 레이아웃 이동이 발생한 DOM 요소와 직접적으로 연결되지 않을 수 있습니다. 예를 들어 새로 삽입된 요소가 그 아래 콘텐츠를 밀어낸 경우, sources 속성은 이동된 요소만을 보고하며 삽입된 요소는 보고하지 않습니다.
우리는 사용자 에이전트가 의미 있는 “근본 원인” 표시를 위해 필요한 수준까지 불안정성의 근거를 파악하는 것은 현실적으로 어렵다고 생각합니다. 그러나 이 API에서 제공되는 더 직접적인 이동 요소 정보도, 레이아웃 불안정성 문제를 진단하는 개발자에게 상당한 도움이 될 것으로 기대합니다.
1.3. 사용 예시
이 절은 비규범적입니다.
let perFrameLayoutShiftData= []; let cumulativeLayoutShiftScore= 0 ; function updateCLS( entries) { for ( const entryof entries) { // Only count layout shifts without recent user input. if ( entry. hadRecentInput) return ; perFrameLayoutShiftData. push({ score: entry. value, timestamp: entry. startTime}); cumulativeLayoutShiftScore+= entry. value; // Sources are sorted by impact area in descending order. // The first element contributed most to the layout shift. if ( entry. sources&& entry. sources. length> 0 ) { console. log( 'Largest contributing element:' , entry. sources[ 0 ]. node); } } } // Observe all layout shift occurrences. const observer= new PerformanceObserver(( list) => { updateCLS( list. getEntries()); }); observer. observe({ type: 'layout-shift' , buffered: true }); // Send final data to an analytics back end once the page is hidden. document. addEventListener( 'visibilitychange' , () => { if ( document. visibilityState=== 'hidden' ) { // Force any pending records to be dispatched. updateCLS( observer. takeRecords()); // Send data to your analytics back end (assumes `sendToAnalytics` is // defined elsewhere). sendToAnalytics({ perFrameLayoutShiftData, cumulativeLayoutShiftScore}); } });
레이아웃 이동 점수는 “점프 현상”의 사용자 경험과 대략적으로 상관관계를 갖는 단일 지표일 뿐입니다.
개발자는 레이아웃 이동 점수 간의 작은 변동에 너무 신경 쓰지 않아도 됩니다. 이 측정치는 고정밀 값을 목적으로 하지 않으며, 사용자 에이전트는 계산 효율을 위해 정밀도를 타협할 수 있습니다. 또한, 이 측정치의 정의는 시간이 지남에 따라 변경될 수도 있습니다.
2. 용어
2.1. 기본 개념
시작 지점은
좌표 공간 C에서 Node
N에 대해 다음과 같이 정의된다:
-
만약 N이 하나 이상의 박스(boxes)를 생성하는
Element라면, C 내에서 N의 시작 지점은 flow-relative 기준 처음 프래그먼트의 시작 모퉁이에서 C의 원점까지의 픽셀 단위 2차원 오프셋이다. 해당 박스는 주요 박스(principal box)이다. -
만약 N이 텍스트 노드(text node)라면, C 내 N의 시작 지점은 C의 원점에서 flow-relative 기준 처음 라인 박스(line box)의 시작 모퉁이까지의 픽셀 단위 2차원 오프셋이다.
변환에 무관한 시작
지점(transform-indifferent starting point)은
좌표 공간 C의 Node
N에 대해, 모든 변형된 요소(transformed element)가 단위 변환 행렬(transformation matrix)을 가진다고 가정한 상태에서
N의 시작 지점이다.
참고: 노드가 이동했는지 판단하기 위해 변환을 포함한 시작 지점과 변환을 제외한 시작 지점 모두를 고려하는데, 이는 변환(transform) 변화 때문에만 노드가 불안정하다고 판단되지 않도록 보장하기 위함이다. 하지만 CSS transform은 시각적 표현 계산과 뷰포트 외부 포인트의 제외에는 항상 반영된다.
시각적 표현(visual
representation)은
Node
N에 대해 다음과 같이 정의된다:
-
만약 N이 하나 이상의 박스를 생성하는
Element라면, N의 시각적 표현은 박스의 프래그먼트(fragment) 중 어느 것이든 해당되는 경계 안의 모든 점들의 집합으로, 뷰포트의 좌표 공간에서, 뷰포트 밖의 점들은 제외한다. -
만약 N이 텍스트 노드라면, N이 생성한 모든 라인 박스(line box)의 경계 안의 점 전체의 집합이다. 이때 역시 뷰포트 내 좌표 공간이며, 뷰포트 밖의 점들은 제외된다.
조건이 이전 프레임에서 성립한다는 것은, 레이아웃 이동을 보고(report the layout shift) 알고리즘의 가장 최근 완료 즉시 시점에서 해당 조건이 참임을 의미한다.
Node
N의 좌표 공간 C에서의
이전 프레임 시작
지점은,
이전 프레임에서
N의 시작 지점이었던 점이다.
Node
N의 좌표 공간 C에서의
이전 프레임 변환 무관 시작 지점은,
이전 프레임에서
변환에 무관한 시작 지점이었던 점이다.
Node
N에 대해
이전 프레임
시각적 표현은,
이전 프레임에서
시각적 표현이었던 점들의
집합이다.
각 사용자 에이전트는 정수형 유의미 픽셀 수(number of pixels to significance)를 정의한다. 이 값은 움직임이 레이아웃 이동으로 간주되는지 판단하는 데 사용된다. 이를 구현별로 허용하여, 사용자 에이전트가 성능이나 사용자 경험에 따라 조정할 수 있다.
점 A와 B가 유의미 픽셀 수 이상, 수평 또는 수직 방향 한쪽에서 픽셀 단위로 차이가 있으면, A는 B와 의미있게 다르다(differs significantly)고 한다.
참고: 크롬은 유의미 픽셀 수를 3으로 정의함.
2.2. 불안정한 노드
Node
N은 좌표 공간 C에서 이동했다(has shifted)고
한다, 다음의 조건을 모두 만족할 때:
-
N의 시작 지점과 이전 프레임 시작 지점이 의미있게 다르다이고,
그렇지 않으면 N은 이동하지 않았다(has not shifted)고 한다.
Node
N이 다음 조건을 모두 만족하면 불안정 후보(unstable-candidate)이다:
-
N은 아래 중 하나여야 한다.
-
현재와 이전 프레임 모두에서 N의 visibility 속성의 computed value가 "visible"이다.
-
현재 및 이전 프레임의 N과 그 조상 모두의 opacity 속성의 computed value가 0이 아니다.
-
N이 이동했다 (coordinate space: initial containing block),
-
아래 조건을 모두 만족하는
ElementP가 존재하지 않는다:-
현재와 이전 프레임 모두에서, P는 N의 containing block chain에 포함되며,
-
현재 및 이전 프레임에 P는 스크롤 가능 오버플로우 영역이 있으며,
-
P가 불안정 후보가 아니고,
-
N이 이동하지 않았다 (P의 스크롤 가능 오버플로우 영역 기준)
-
참고: 스크롤 가능 오버플로우 영역과 관련된 이 조건은 노드가 단순히 스크롤됐다는 이유만으로 불안정하다고 판단되는 것을 방지하기 위한 것이다.
Node
N이 불안정
후보이면서
인라인 클립 교차자(inline clip
crosser)가 아니면
불안정(unstable)하다 고 한다.
Node
N이 아래 조건을 만족하면 인라인 클립 교차자(inline clip crosser)이다:
-
N이 불안정 후보이다;
-
N의 시각적 표현 혹은 이전 프레임 시각적 표현 중 하나가 비어있으며,
-
그리고, 만일 의미있게 다르다 정의문의 “가로 또는 세로 방향”을 (만약 block axis가 세로라면) “세로 방향”으로 혹은 (block axis가 가로라면) “가로 방향”으로 바꾼다면 N이 불안정 후보가 되지 않게 된다.
참고: 인라인 클립 교차자 예시는, 요소가 인라인 방향으로 클립 경계를 넘어서 보이거나 사라질 때와 같다. 이러한 요소는 block 흐름 방향으로 이동하지 않는 한, 불안정 노드 집합에서 제외된다. 이는 일부 “캐러셀” UI 컨트롤을 쉽게 구현할 수 있도록 돕는다.
Document
D의 불안정 노드
집합(unstable node set)은,
D의 모든 불안정(unstable) shadow-including descendant를 포함하는 집합이다.
참고: 첫 프레임에서는 노드마다 이전 프레임 시작 지점이 존재하지 않으므로, 이 시점의 불안정 노드 집합은 비어있다.
2.3. 레이아웃 이동 값
뷰포트 기준 거리는 시각적 뷰포트 너비와 시각적 뷰포트 높이 중 큰 값이다.
이동 벡터(move vector)는 Node
N에 대해
픽셀
단위의 2차원
오프셋으로 정의된다:
-
좌표 공간이 뷰포트일 때, N의 이전 프레임 시작 지점에서,
-
동일한 좌표 공간의 N의 시작 지점까지.
이동 거리(move distance)는
Node
N에 대해 다음 두 값 중 큰 값이다.
최대 이동 거리(maximum move
distance)는
Document
D의 불안정 노드 집합
내
각 Node의
이동 거리 중 가장 큰 값이다. 만약 불안정 노드 집합이 비어 있으면
0이다.
거리 비율(distance
fraction)은
Document
D에 대해 다음 두 값 중 작은 값이다.
노드 영향 영역(node impact
region)은
불안정 Node
N에 대해 다음 두 집합의 합집합이다:
-
N의 시각적 표현 내의 모든 점,
-
N의 이전 프레임 시각적 표현 내의 모든 점
영향 영역(impact region)은
Document
D의 불안정 노드
집합 내
각 Node의
노드 영향 영역
전체 점의 집합이다.
영향 비율(impact fraction)은
Document
D의
영향 영역의 면적을
뷰포트의
면적으로 나눈 값이다.
(뷰포트의 면적이 0이면 0)
참고: 영향 영역의 면적 계산은 2차원 Klee 측정 문제의 한 예시다. sweep line 및 세그먼트 트리를 사용하는 시간복잡도 O(n lg n) 알고리즘 예시는 여기에 설명되어 있다.
레이아웃 이동 값(layout shift
value)은
Document
D에 대해
영향 비율에
거리 비율을 곱한 값이다.
참고: 레이아웃 이동 값은 레이아웃 불안정성이 뷰포트에서 차지하는 면적 비율과, 실제로 움직인 최대 거리 모두를 고려한다. 크기가 큰 요소가 소폭 이동해도 페이지의 불안정성 체감에는 미치는 영향이 작을 수 있음을 반영한다.
2.4. 입력 배제
배제 입력(excluding input)은 입력 장치에서 발생한 사용자의 문서와의 적극적 상호작용을 알리는 이벤트이거나, 뷰포트 크기를 직접 변경하는 이벤트이다.
배제 입력에는 보통 mousedown, keydown, pointerdown와 change 이벤트가 포함된다. 단, flick 또는 스크롤 제스처를 시작하거나 갱신하는 것만이 효과인 이벤트는 배제 입력이 아니다.
사용자 에이전트는 pointerdown 이벤트 이후, 해당 이벤트가 flick 또는 스크롤 제스처의 시작이 아님을 알 때까지 레이아웃 이동 보고를 지연할 수 있다.
mousemove와 pointermove 이벤트도 배제 입력에 해당하지 않는다.
3. LayoutShift
인터페이스
[Exposed =Window ]interface :LayoutShift PerformanceEntry {readonly attribute double value ;readonly attribute boolean hadRecentInput ;readonly attribute DOMHighResTimeStamp lastInputTime ;readonly attribute FrozenArray <LayoutShiftAttribution >sources ; [Default ]object (); };toJSON
모든 속성은 레이아웃 이동을 보고 단계에서 할당된 값을 가진다.
sources 속성은
FrozenArray
타입의 LayoutShiftAttribution
객체 배열을 반환한다. 영향 영역 크기 기준 내림차순 정렬되며,
첫 번째 요소가 노드 영향 영역이
가장 크고
레이아웃 이동에 가장 크게 기여한 요소를 의미한다.
Layout Instability API를 구현하는 사용자 에이전트는
Window 컨텍스트의
supportedEntryTypes에
를
반드시 포함해야 한다.
이를 통해 개발자는 Layout Instability API 지원 여부를 감지할 수 있다.
4. LayoutShiftAttribution
인터페이스
[Exposed =Window ]interface {LayoutShiftAttribution readonly attribute Node ?;node readonly attribute DOMRectReadOnly previousRect ;readonly attribute DOMRectReadOnly currentRect ; };
참고: previousRect
와 currentRect
속성은 CSS 픽셀 단위로 보고된다.
이는 getBoundingClientRect(),
IntersectionObserver,
ResizeObserver
등 다른 Web Platform API와 일치한다.
이로 인해 레이아웃 이동 측정과 다른 DOM 측정의 좌표 일관성을 보다 쉽게 확보할 수 있다.
각 LayoutShiftAttribution
는 Node
(이를 관련 노드(associated
node)라 한다)와 연관되어 있다.
LayoutShiftAttribution
인스턴스 A의 node 속성 getter는, A의 get an
element 알고리즘을
A의 관련 노드 및
해당 node document를 인수로 하여 호출한 결과를 반환한다.
참고: get an element 알고리즘을 사용함으로써, node 속성은 관련 노드가 더 이상 연결되어 있지 않거나 shadow root 내부에 있을 경우 null이 된다.
get an element 알고리즘은 Element Timing 명세에서 분리되어 이 명세에서도 재사용 가능해야 함.
get an
element 알고리즘은 Node
타입을 수용하도록 일반화되어야 하며
Element
타입에 한정되지 않아야 한다.
previousRect와 currentRect 속성은 귀속 생성(create the attribution) 단계에서 할당된 값을 가진다.
5. 처리 모델
렌더링 갱신(update the rendering) 단계 내에서, 이벤트 루프 처리 모델(event loop processing model)의 일부로, Layout Instability API를 구현하는 사용자 에이전트는 페인트 타이밍 기록(mark paint timing) 단계 이후 다음 단계를 반드시 수행해야 한다:
-
모든 fully active
Documentdocs에 대해, 해당Document의 레이아웃 이동을 보고 알고리즘을 호출한다.
5.1. 레이아웃 이동 보고
Document
D에 대해 레이아웃
이동을 보고(report the layout shift)할 때,
다음 절차를 수행한다:
-
현재 D의 레이아웃 이동 값이 0이 아니라면:
-
D의 관련 realm에서,
LayoutShift객체 newEntry를 새로 생성한다. -
newEntry의
name속성에를 할당한다."layout-shift" -
newEntry의
entryType속성에를 할당한다."layout-shift" -
newEntry의
startTime속성에 현재 고해상도 시간(current high resolution time)을 D의 관련 전역 객체(relevant global object) 기준으로 할당한다. -
newEntry의
duration속성에 0을 할당한다. -
newEntry의
value속성에 현재 D의 레이아웃 이동 값을 할당한다. -
newEntry의
lastInputTime속성에 가장 최근 배제 입력의 시각을 할당한다. 세션 중 배제 입력이 일어나지 않았다면 0으로 한다. -
newEntry의
hadRecentInput속성에를 할당(단,true lastInputTime이 직전 500ms 미만이면), 아니면를 할당.false -
newEntry의
sources속성에 D에 대한 레이아웃 이동 소스 보고(report the layout shift sources) 알고리즘 호출 결과를 할당. -
PerformanceEntry 큐(queue a PerformanceEntry) newEntry 객체.
-
5.2. 레이아웃 이동 소스 보고
Document
D에 대해
레이아웃 이동 소스
보고(report the layout shift sources)를 요청받으면,
다음 단계를 수행한다:
-
D의 불안정 노드 집합의 각 멤버 N마다 아래를 수행한다:
-
만약 C의 어떤 멤버 existingNode가 존재하여 N의 노드 영향 영역이 existingNode의 노드 영향 영역의 부분집합이라면, 다음 단계로 넘어간다.
-
그렇지 않고, C의 멤버 existingNode 중 existingNode의 노드 영향 영역이 N의 노드 영향 영역의 부분집합이면, replace(교체)를 통해 해당 existingNode를 C에서 N으로 바꾼다.
-
그것도 아니라면, C의 멤버가 5개 미만이면 append(추가)로 N을 C에 넣는다.
참고: 5개라는 값은 임의이지만, 메모리 제한과 노드 셋 과다 노출 방지 사이에서 상세 귀속 정보를 제공하는 타협값이다.
-
그 외의 경우, 아래 단계를 실행한다:
-
-
내림차순 정렬 로 C를 정렬한다. 노드 영향 영역 면적이 작은 것이 큰 것보다 앞에 오도록 한다.
참고: 이를 통해 sources 속성이 영향 영역 내림차순으로, 레이아웃 이동에 가장 크게 기여한 요소가 가장 먼저 노출되도록 보장한다.
-
C 각 멤버에 대해 귀속 생성(create the attribution) 알고리즘을 실행해 만들어진
FrozenArray타입의LayoutShiftAttribution객체 배열을 반환한다.
Node
N에 대해 귀속
생성(create the attribution)을 요청받으면
아래 단계를 수행한다:
-
N의 관련 realm에서
LayoutShiftAttribution객체 A를 생성한다. -
A의 관련 노드를 N으로 설정한다.
-
N의 이전 프레임 시각적 표현을 포함하는 최소의 사각형(Rectangle)(단위: CSS 픽셀)을
previousRect속성에 설정한다. -
N의 시각적 표현을 포함하는 최소의 사각형(Rectangle)(단위: CSS 픽셀)을
currentRect속성에 설정한다. -
A를 반환한다.
6. 보안 및 개인정보 보호 고려사항
레이아웃 불안정성(layout instability)은 resource timing과 간접적으로 관련이 있다. 느린 리소스는 그렇지 않았을 중간 레이아웃을 유발할 수 있다. Resource timing 정보는 악의적인 사이트가 통계적 핑거프린팅(statistical fingerprinting)에 악용할 수 있다. 레이아웃 불안정성 API는 현재 브라우징 컨텍스트에 대해서만 불안정성을 보고한다. 여러 브라우징 컨텍스트의 점수를 직접 합산하지 않는다. 개발자가 직접 합산 코드를 구현할 수는 있으나, origin이 다른 브라우징 컨텍스트들에서는 협력 없이는 점수 공유가 불가능하다.