1. 소개
Container Timing API를 사용하면 DOM에서 표시된 섹션이 화면에 표시되고 초기 페인트가
완료되는 시점을 모니터링할 수 있다. 개발자는 DOM의 하위 섹션에 [^containertiming^]
속성(Element Timing API의 elementtiming과 유사함)을 지정하고 해당 섹션이
처음으로 페인트되었을 때 성능 항목을 수신할 수 있다.
이 API를 통해 개발자는 페이지 안의 여러 컴포넌트 타이밍을 측정할 수 있다. 개발자가 애플리케이션을 점점 더 컴포넌트 단위로 구성함에 따라, 애플리케이션이나 웹 페이지의 하위 섹션에 대한 성능을 측정하려는 수요가 커지고 있다.
Element Timing과 달리, DOM의 한 섹션이 언제 페인트를 완료했는지 렌더러가 알 수는 없다(향후 변경,
새 이미지에 대한 비동기 요청, 느리게 로드되는 버튼 등이 있을 수 있음). 따라서 이 API는 업데이트가
있었을 때 PerformanceEntry
객체 형식의 후보를 내보낸다.
2. 동기
개발자는 테이블, 위젯 또는 기타 컴포넌트와 같은 DOM의 하위 섹션이 언제 페인트되었는지 측정하여 페인트 시간을 추적하고 분석 시스템에 제출하려 한다. 현재 웹 API는 이를 충분히 지원하지 않는다.
-
Element Timing은 지원할 수 있는 범위가 제한되어 있으며 전체 섹션에는 사용할 수 없다.
-
Largest Contentful Paint(LCP)는 페이지의 특정 부분이 언제 로드되었는지 측정하는 데 충분히 유용하지 않다.
-
사용자 공간 폴리필에는 다음과 같은 상당한 단점이 있다.
-
페인트 전에 요소를 표시해야 함(서버 측 변경 또는 렌더링 차단이 필요함)
-
새로 삽입된 요소를 포착하기 위해 MutationObserver가 필요함
-
문서 head에서 실행되어야 하므로 첫 페인트까지의 시간이 증가함
-
브라우저 내장 2D 엔진과 비교해 사각형 추적 효율이 낮을 수 있음
-
웹 작성자는 자신의 도메인을 누구보다 잘 알고 있으며, 사용자나 조직이 이해할 수 있는 방식으로 자신들의 콘텐츠 블록 성능을 전달하려 한다(예: "첫 트윗까지의 시간").
2.1. 수명 주기
이 예시 수명 주기에서 컴포넌트는 여러 콘텐츠 조각을 서로 다른 시점에 페인트하며, 이러한 각 시점은 업데이트된 정보가 포함된 새PerformanceContainerTiming
항목을 생성한다.
그러나 한 영역이 한 번 페인트되면, 같은 영역의 후속 페인트는 새 항목을 생성하지 않는다.
3. 사용 예
다음 예시는 컨테이너 루트를 등록하고 그 페인트 타이밍을 관찰하는 방법을 보여준다.
< div containertiming = "foobar" > < main > ...</ main > < aside > ...</ aside > </ div > < script > const observer= new PerformanceObserver(( list) => { let perfEntries= list. getEntries(); for ( const entryof perfEntries) { console. log( '컨테이너가 페인트됨:' , entry. identifier, '시각' , entry. startTime, '크기:' , entry. size); } }); observer. observe({ entryTypes: [ "container" ] }); </ script >
이 속성은 요소가 문서에 추가되기 전에 설정해야 한다(HTML에서 설정하거나, JavaScript에서 설정하는 경우 문서에 추가하기 전에 설정한다). 나중에 속성을 설정하면 이후에 발생하는 이벤트와 향후 페인트만 캡처한다.
3.1. 하위 트리 무시
< div containertiming = "foobar" > < main > ...</ main > <!-- Aside 업데이트는 컨테이너 타이밍 이벤트를 트리거하지 않는다 --> < aside containertimingignore > ...</ aside > </ div >
4. 용어
컨테이너 루트는
[^containertiming^] 속성을 가진 HTMLElement이다.
무시되는 하위 트리는
[^containertimingignore^] 속성을 가진 HTMLElement를
루트로 하는
하위 트리이다.
페인트된 영역은
처음 관찰된 이후 누적된 컨테이너 루트의 페인트된
모든 부분을 나타내는
영역(사각형의 집합)이며, CSS 픽셀로 표현된다. 페인트된 영역은 뷰포트 좌표 공간에서 유지된다. 컨테이너 루트 요소가 이동하는 경우
(예: 레이아웃 변경 또는 moveBefore()로
인해),
이전에 누적된 사각형은 조정되지 않으며, 영역은 뷰포트 좌표에서 계속 확장된다.
컨테이너 타이밍 API는 컨테이너 루트가 화면에 페인트되는 시점에 관한 타이밍 정보를 제공한다.
5. PerformanceContainerTiming
인터페이스
[Exposed =Window ]interface :PerformanceContainerTiming PerformanceEntry {readonly attribute DOMString identifier ;readonly attribute DOMRectReadOnly intersectionRect ;readonly attribute unsigned long long size ;readonly attribute DOMHighResTimeStamp firstRenderTime ;readonly attribute HTMLElement ?lastPaintedElement ;readonly attribute HTMLElement ?rootElement ; };PerformanceContainerTiming includes PaintTimingMixin ;
참고: intersectionRect의
좌표와 size는
CSS 픽셀 단위로 표현되며(size의
경우 제곱 CSS 픽셀),
페인트된
영역의 좌표 공간 및 교차 사각형 알고리즘이 생성하는 단위와 일치한다.
PerformanceContainerTiming
객체에는 다음과 같은 관련 개념이 있다:
-
처음에 빈 문자열로 설정되는 identifier.
-
처음에 모든 값이 0으로 설정된
DOMRectReadOnly로 설정되는 intersectionRect. -
처음에 0으로 설정되는 size.
-
처음에 0으로 설정되는 firstRenderTime.
-
처음에 0으로 설정되는 renderTime.
-
연결된
HTMLElement를 포함하며, 처음에null로 설정되는 lastPaintedElement. -
컨테이너 루트인 연결된
HTMLElement를 포함하며, 처음에null로 설정되는 rootElement.
entryType
속성의 getter는 DOMString
"container"를 반환해야 한다.
name
속성의 getter는 빈 문자열을 반환해야 한다.
duration
속성은 0을 반환해야 한다.
startTime
속성의 getter는 this의 renderTime 값을 반환해야 한다.
identifier 속성은
this의 identifier 값을 반환해야 한다.
intersectionRect
속성은 this의 intersectionRect 값을 반환해야 한다.
size 속성은
this의 size 값을 반환해야
한다.
firstRenderTime
속성은 this의 firstRenderTime 값을 반환해야 한다.
lastPaintedElement
속성은 this의 lastPaintedElement 값을 반환해야 한다.
rootElement 속성은
this의 rootElement 값을 반환해야 한다.
참고: 사용자 에이전트는 제거된 콘텐츠로 인해
메모리 누수가 발생하지 않도록 컨테이너 루트 레코드 맵을 유지해야 한다. 특히 항목의 수명을 HTMLElement에
대한
약한 포인터와 연결하여, HTMLElement가
제거된 후 언젠가 정리될 수 있도록 할 수 있다. 이 맵은 웹 개발자에게 노출되지 않으므로 가비지 컬렉션 시점이 노출되지 않는다.
6. 처리 모델
참고: 컨테이너 타이밍 API를 구현하는 사용자 에이전트는
supportedEntryTypes에
"container"를 포함해야 한다.
이는 Window
컨텍스트에 적용된다.
이를 통해 개발자는 컨테이너 타이밍 지원 여부를 감지할 수 있다.
6.1. 문서별 상태
각 Document에
대해,
사용자 에이전트는 컨테이너 루트 HTMLElement를
컨테이너 타이밍 레코드 객체에 매핑하는 컨테이너 루트 레코드 맵을
유지해야 한다.
6.2. HTMLElement
인터페이스 확장
이 절은 [DOM] 명세가 수정되면 제거될 예정이다.
다음과 같이 HTMLElement
인터페이스를 확장한다:
partial interface HTMLElement { [CEReactions ,Reflect ]attribute DOMString containerTiming ; [CEReactions ,Reflect ]attribute boolean containerTimingIgnore ; };
containerTiming 속성은 요소를 컨테이너 루트로 식별하는 DOMString이다.
그 값은 해당 PerformanceContainerTiming
항목의 identifier가
된다.
containerTimingIgnore 속성이
존재하면, 요소와 그 자손을 조상 컨테이너
루트의 컨테이너 타이밍
측정에 기여해서는 안 되는 무시되는 하위
트리로 표시한다.
6.3. 컨테이너 타이밍 레코드
이 명세는 처리 모델에서 사용되는 내부 데이터 구조를 정의한다.
-
paintTimingInfo이며, 이는 페인트 타이밍 정보이다.
-
identifier이며, 이는
DOMString이다. -
paintedRegion이며, 이는 페인트된 영역이고, 초기에는 비어 있다.
-
lastNewPaintedAreaPaintTimingInfo 이며, 이는 페인트 타이밍 정보이고, 초기에는 설정되어 있지 않다.
-
lastNewPaintedAreaElement이며, 이는
HTMLElement또는 null이고, 초기에는 null이다. -
lastNewPaintedAreaSize이며, 이는 숫자이고, 초기에는 0이다.
-
hasPendingChanges이며, 이는 불리언이고, 초기에는 false이다.
DOMString
identifier가 주어졌을 때 컨테이너
타이밍 레코드를 생성하려면 다음 단계를 수행한다:
-
record를 새로운 컨테이너 타이밍 레코드로 둔다.
-
record의 paintTimingInfo를 paintTimingInfo로 설정한다.
-
record의 identifier를 identifier로 설정한다.
-
record를 반환한다.
6.4. 컨테이너 루트 등록
[^containertiming^] 콘텐츠 속성을 가진 HTMLElement가
문서에 연결될 때:
-
사용자 에이전트는 해당 요소를 컨테이너 루트로 등록해야 한다.
-
사용자 에이전트는 컨테이너 루트의 하위 트리 내에서 발생하는 모든 페인트 작업을 추적하되, 모든 무시된 하위 트리는 제외해야 한다.
6.5. containertiming 속성 제거
HTMLElement
element에서 제거되면, 다음 단계를 수행한다:
-
document를 element의 노드 문서로 설정한다.
-
document의 컨테이너 루트 레코드 맵에 element에 대한 항목이 포함되어 있으면, 해당 항목을 제거한다.
참고: 나중에 동일한 요소에 [^containertiming^] 속성이 다시 추가되면 다음 페인트 시 새로운 컨테이너 타이밍 레코드가 생성된다. 페인트된 영역은 새로 시작하며, 이전 페인트 데이터는 보존되지 않는다.
6.6. 컨테이너 루트 연결 해제
HTMLElement
element가 문서에서 연결 해제되면, 다음 단계를 수행한다:
-
document를 element의 노드 문서로 설정한다.
-
document의 컨테이너 루트 레코드 맵에 element에 대한 항목이 포함되어 있으면, 해당 항목을 제거한다.
참고: 요소가 여전히 [^containertiming^] 속성을 가진 상태로 문서에 다시 연결되면 새로운 컨테이너 루트 등록으로 처리된다. 다음 페인트 시 새로운 컨테이너 타이밍 레코드가 새로운 페인트된 영역과 함께 생성된다.
6.7. 컨테이너 타이밍을 위해 페인트된 요소 처리
Document
document, 페인트 타이밍 정보 paintTimingInfo, HTMLElement
element 및 DOMRectReadOnly
intersectionRect가 주어지면, 다음 단계를 수행한다:
-
element가 컨테이너 타이밍에 기여하지 않으면, 반환한다.
-
containerRoot를 element가 주어졌을 때 컨테이너 루트 요소를 가져오기의 결과로 설정한다.
-
containerRoot가 null이면, 반환한다.
-
record를 document의 컨테이너 루트 레코드 맵에서 containerRoot에 해당하는 항목으로 설정한다. 그러한 항목이 없으면, record를 paintTimingInfo와 containerRoot의 [^containertiming^] 콘텐츠 속성 값이 주어졌을 때 컨테이너 타이밍 레코드를 생성하기의 결과로 설정한 다음, (containerRoot → record)를 document의 컨테이너 루트 레코드 맵에 추가한다.
-
enclosingRect를 intersectionRect를 둘러싸는 가장 작은 사각형으로 설정한다.
-
document, containerRoot, element, enclosingRect 및 paintTimingInfo가 주어졌을 때 record에 대해 마지막 새 페인트 영역을 필요에 따라 업데이트하기를 수행한다.
-
Document에 보류 중인 컨테이너 타이밍 변경 사항이 있는 것으로 표시한다.
참고: 이 알고리즘은 페인트하는 각 이미지 또는 텍스트 노드에 대해 호출된다. 교차 사각형은 요소를 대상으로 하고 뷰포트를 루트로 하여 교차 사각형 알고리즘을 사용해 계산한 후 시각적 뷰포트와 교차시켜야 한다. 텍스트 노드의 경우 교차 사각형은 소유된 텍스트 노드 집합에 있는 모든 텍스트 노드의 테두리 상자를 포함하는 가장 작은 사각형을 시각적 뷰포트와 교차시킨 것이다.
6.8. 컨테이너 타이밍 항목 방출
Document
document에 대한 컨테이너 타이밍 항목을 방출하도록 요청받으면 다음 단계를 수행한다. 이는 모든 페인트
작업이 처리된 후 프레임당 한 번 호출해야 한다:
-
document에 보류 중인 컨테이너 타이밍 변경 사항이 없으면 반환한다.
-
document의 컨테이너 루트 레코드 맵의 containerRoot → record 각각에 대해 반복한다:
-
record의 hasPendingChanges가 false이면 계속한다.
-
record와 containerRoot가 주어졌을 때 Container Timing 항목을 생성한다.
-
record의 hasPendingChanges를 false로 설정한다.
-
record의 lastNewPaintedAreaElement를 null로 설정한다.
-
record의 lastNewPaintedAreaSize를 0으로 설정한다.
-
-
Document가 더 이상 보류 중인 컨테이너 타이밍 변경 사항을 갖지 않는 것으로 표시한다.
참고: 페인트된 요소마다 여러 항목을 방출할 수 있는
일부 다른 페인트 타이밍 API와 달리, Container Timing은 각 컨테이너 루트의 페인트된 영역을 누적하고
프레임마다 컨테이너 루트당 최대 하나의 PerformanceContainerTiming
항목을 방출한다. 이 일괄 처리 방식은 더 효율적이며 컨테이너의 페인트 상태를 전체적으로 보여준다.
6.9. 상위 컨테이너 루트 Element 가져오기
HTMLElement
element가 주어졌을 때 부모
컨테이너 루트 요소를 가져오려면, 다음 단계를 수행한다:
-
parent를 element의 parentElement로 설정한다.
-
parent가 null이면, null을 반환한다.
-
parent가 주어졌을 때 컨테이너 루트 요소를 가져오기의 결과를 반환한다.
6.10. 컨테이너 타이밍에 기여함
HTMLElement는
다음 조건이 모두 참인 경우 컨테이너 루트의
컨테이너 타이밍에 기여한다:
-
컨테이너 루트의 자손이다.
-
무시되는 하위 트리 내에 있지 않다.
-
섀도 트리에 있지 않다.
HTMLElement
element가 컨테이너 루트
containerRoot의 컨테이너 타이밍에 기여하는지 확인하려면:
-
element가 null이면, false를 반환한다.
-
element가 섀도 트리에 있으면, false를 반환한다.
-
element가 containerRoot의 자손이 아니면, false를 반환한다.
-
element가 무시되는 하위 트리 내에 있으면, false를 반환한다.
-
true를 반환한다.
6.11. 컨테이너 루트 Element 가져오기
HTMLElement
element가 주어졌을 때 컨테이너
루트 요소를 가져오려면, 다음 단계를 수행한다:
-
element가 null이면, null을 반환한다.
-
element의 [^containertiming^] 콘텐츠 속성이 존재하면, element를 반환한다.
-
element의 parentElement가 null이 아니면, element의 parentElement가 주어졌을 때 컨테이너 루트 요소를 가져오기의 결과를 반환한다.
-
null을 반환한다.
6.12. 마지막 새 페인트 영역을 필요시 업데이트
Document
document, 컨테이너
루트 HTMLElement
containerRoot, HTMLElement
element, DOMRectReadOnly
enclosingRect 및 페인트 타이밍 정보 paintTimingInfo가
주어졌을 때 컨테이너 타이밍 레코드
record에 대해 다음 단계를 수행한다:
-
paintedRegion을 record의 paintedRegion으로 한다.
-
paintedRegion이 enclosingRect를 완전히 포함하면 반환한다.
-
newPaintedArea를 enclosingRect에서 아직 paintedRegion에 포함되지 않은 영역의 면적으로 한다.
-
record의 paintedRegion을 paintedRegion과 enclosingRect의 합집합으로 설정한다.
-
record의 lastNewPaintedAreaPaintTimingInfo 를 paintTimingInfo로 설정한다.
-
newPaintedArea가 record의 lastNewPaintedAreaSize보다 크면:
-
record의 lastNewPaintedAreaElement 를 element로 설정한다.
-
record의 lastNewPaintedAreaSize 를 newPaintedArea로 설정한다.
-
-
record의 hasPendingChanges를 true로 설정한다.
-
containerRoot의 [^containertimingignore^] 콘텐츠 속성이 있으면 반환한다.
-
parentContainerRoot를 containerRoot가 주어졌을 때 상위 컨테이너 루트 요소를 가져오는 결과로 한다.
-
parentContainerRoot가 null이면 반환한다.
-
parentRecord를 parentContainerRoot에 대한 document의 컨테이너 루트 레코드 맵의 항목으로 한다. 그러한 항목이 없으면 paintTimingInfo와 parentContainerRoot의 [^containertiming^] 콘텐츠 속성 값이 주어졌을 때 컨테이너 타이밍 레코드를 생성한 결과로 parentRecord를 설정한 다음, (parentContainerRoot → parentRecord)를 document의 컨테이너 루트 레코드 맵에 추가한다.
-
document, parentContainerRoot, element, enclosingRect 및 paintTimingInfo가 주어졌을 때 parentRecord에 대해 마지막 새 페인트 영역을 업데이트할 수 있다.
참고: lastPaintedElement는
단일 렌더링 프레임 내에서 가장 큰 새 페인트 영역에 기여한 요소이다. 이를 통해
구현별 페인트 순서에 의존하지 않게 된다. lastPaintedElement는
그 자체로 독립적인 성능 메트릭이라기보다 크거나 복잡한 컨테이너 루트(예:
테이블)에서 무엇이 업데이트를 유발하는지 조사하는 개발자를 위한 디버깅 보조 수단이다. lastNewPaintedAreaSize는
항목이 방출된 후 재설정되어 각 프레임의 비교가 새로 시작되도록 한다.
참고: 이 알고리즘은 크기와 관계없이 페인트된
영역의 모든 변경을 보고한다. 페인트된 영역이 1픽셀만 변경되어도 새로운 PerformanceContainerTiming
항목이 큐에 추가된다. 작은 변경을 필터링하려는 개발자는 항목 간의 size
값을 비교하여 필터링할 수 있다.
6.13. 컨테이너 타이밍 항목 생성
HTMLElement
containerRoot가 주어지면, 사용자 에이전트는 다음 단계를 수행해야 한다:
-
entry를 다음과 같이 설정된 새로운
PerformanceContainerTiming항목으로 둔다:-
entryType을 "container"로 설정한다 -
name을 빈 문자열로 설정한다 -
startTime을 record의 lastNewPaintedAreaPaintTimingInfo의paintTime으로 설정한다 -
duration을 0으로 설정한다 -
identifier를 record의 identifier로 설정한다 -
firstRenderTime을 record의 paintTimingInfo의paintTime으로 설정한다 -
intersectionRect를 record의 paintedRegion의 경계 사각형으로 설정한다 -
size를 record의 paintedRegion의 총 면적을 제곱 CSS 픽셀 단위로 나타낸 값으로 설정한다 -
lastPaintedElement를 record의 lastNewPaintedAreaElement로 설정한다 -
rootElement를 containerRoot로 설정한다
-
-
entry PerformanceEntry를 큐에 추가한다.
7. 보안 및 개인정보 보호 고려사항
7.1. 교차 출처 제한
이 API는 교차 출처 경계를 존중한다.
-
교차 출처 iframe에 속한 요소는 부모 프레임에 노출되지 않는다.
-
개발자가
postMessage를 통해 명시적으로 전달하지 않는 한, 타이밍 정보는 프레임 경계를 넘지 않는다.
7.2. 정보 노출
이 API가 제공하는 대부분의 정보는 기존 API를 통해 이미 추정할 수 있다.
-
Element Timing은 이미지와 텍스트의 최초 렌더링 시간을 반환한다.
-
Paint Timing API는 관련 타임스탬프를 제공한다.
-
이러한 API들의 조합은 컨테이너 타이밍 정보를 근사할 수 있지만, 효율성은 더 낮다.
이 API는 다음을 노출하지 않는다.
-
렌더링 엔진의 내부 구현 세부사항
-
개발자가 이미 접근 권한을 갖고 있지 않은 요소에 대한 정보
-
기존 Performance API를 통해 이미 사용할 수 있는 것보다 더 세밀한 타이밍 정보
7.3. 타이밍 공격
이 API는 DOMHighResTimeStamp를
사용하며, 이는 다른 Performance API와 일관되게 보안 목적의 해상도 제한을 받을 수 있다.
7.4. 개인정보 보호 고려사항
이 API는 다음을 하지 않는다.
-
사이트 간 사용자 추적을 가능하게 하지 않는다
-
브라우징 기록을 노출하지 않는다
-
사이트가 스크립트 실행을 통해 이미 접근할 수 있는 범위를 넘어서는 사용자 행동 정보를 제공하지 않는다
8. 감사의 말
소중한 피드백과 조언을 주신 다음 분들께 깊이 감사한다.
-
Barry Pollard
-
Michael Mocny
-
Scott Haseley
-
Sergey Chernyshev
-
Bas Schouten