웹 기본에서 재인쇄
브라우저가 페이지를 렌더링하기 전에 먼저 DOM 및 CSSOM 트리를 구축해야 합니다. 따라서 우리는 HTML과 CSS가 모두 가능한 한 빨리 브라우저에 제공되도록 해야 합니다.
바이트 → 문자 → 태그 → 노드 → 객체 모델.
HTML 마크업은 DOM(문서 개체 모델)으로 변환되고, CSS 마크업은 CSSOM(CSS 개체 모델)으로 변환됩니다. DOM과 CSSOM은 독립적인 데이터 구조입니다.
Chrome DevTools Timeline은 DOM 및 CSSOM의 구성 및 처리 오버헤드를 캡처하고 검사할 수 있습니다.
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path</title> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div> </body></html>
일부 텍스트와 이미지가 포함된 일반 HTML 페이지인데, 브라우저는 이 페이지를 어떻게 처리합니까?
HTML 파서의 트리 출력은 DOM 요소와 속성 노드로 구성됩니다. 이는 HTML 문서의 객체 설명이자 HTML 요소와 외부 세계(예: Javascript) 간의 인터페이스이기도 합니다. DOM과 태그는 거의 일대일로 대응됩니다.
변환: 브라우저는 디스크나 네트워크에서 HTML의 원시 바이트를 읽고 이를 파일의 지정된 인코딩(예: UTF-8)에 따라 개별 문자로 변환합니다.
Tokenizing: 브라우저는 문자열을 W3C HTML5 표준에서 지정한 다양한 토큰(예: "", "
" 및 꺾쇠 괄호 안의 기타 문자열)으로 변환합니다. 각 토큰에는 특별한 의미와 일련의 규칙이 있습니다.어휘 분석: 방출된 토큰은 속성과 규칙을 정의하는 "객체"로 변환됩니다.
DOM 빌드: 마지막으로 HTML 태그는 서로 다른 태그 간의 관계를 정의하므로(일부 태그는 다른 태그 내에 포함됨) 생성된 객체는 원래 상위-하위 관계도 캡처하는 트리 데이터 구조 내에 연결됩니다. 마크업에 정의됨: HTML 개체는 body 개체의 부모이고, body는 paragraph 개체의 부모입니다.
전체 프로세스의 최종 출력은 페이지의 문서 객체 모델 (DOM)이며, 이는 페이지의 모든 추가 처리를 위해 브라우저에서 사용됩니다.
브라우저는 HTML 마크업을 처리할 때마다 위의 모든 단계를 완료합니다. 즉, 바이트를 문자로 변환하고, 토큰을 결정하고, 토큰을 노드로 변환한 다음 DOM 트리를 구축합니다. 이 전체 프로세스를 완료하는 데 시간이 좀 걸릴 수 있습니다. 특히 처리할 HTML이 많은 경우에는 더욱 그렇습니다.
Chrome DevTools를 열고 페이지가 로드되는 타임라인을 기록하면 이 단계를 수행하는 데 실제로 시간이 얼마나 걸리는지 확인할 수 있습니다. 위의 예에서는 HTML 바이트 묶음을 DOM 트리로 변환하는 데 약 5밀리초가 걸립니다. 페이지가 큰 경우 이 프로세스에 필요한 시간이 크게 늘어날 수 있습니다. 부드러운 애니메이션을 만들 때 브라우저가 많은 양의 HTML을 처리해야 하면 병목 현상이 쉽게 발생할 수 있습니다.
DOM 트리는 문서 마크업의 속성과 관계를 캡처하지만 렌더링 시 요소가 어떻게 보일지는 알려주지 않습니다. 그것이 CSSOM의 책임입니다.
이 간단한 페이지의 DOM을 구축하는 과정에서 브라우저는 문서 헤드에서 외부 CSS 스타일 시트인 style.css를 참조하는 링크 태그를 발견했습니다. 페이지를 렌더링하는 데 리소스가 필요할 것으로 예상하고 즉시 리소스를 요청하고 다음을 반환합니다.
body { font-size: 16px }p { font-weight: bold }span { color: red }p span { display: none }img { float: right }
HTML 마크업(인라인) 내에서 직접 스타일을 선언할 수도 있지만 HTML과 독립적인 CSS를 사용하면 콘텐츠와 디자인을 독립적인 문제로 처리할 수 있습니다. 디자이너는 CSS를 처리하고 개발자는 HTML에 집중합니다.
HTML로 작업할 때와 마찬가지로 수신된 CSS 규칙을 브라우저가 이해하고 처리할 수 있는 것으로 변환해야 합니다. 따라서 HTML 프로세스를 반복하지만 HTML 대신 CSS의 경우:
CSS 바이트는 문자로 변환된 다음 토큰과 노드로 변환되고 마지막으로 "CSS 개체 모델"(CSSOM)이라는 항목에 연결됩니다. 트리 구조:
CSSOM에 트리 구조가 있는 이유는 무엇인가요? 페이지의 노드 개체에 대한 최종 스타일 세트를 계산할 때 브라우저는 노드에 적용되는 가장 일반적인 규칙으로 시작한 다음(예: 노드가 본문 요소의 하위인 경우 모든 본문 스타일을 적용함) 통과 계산 스타일을 반복적으로 최적화하려면 보다 구체적인 규칙을 적용합니다.
더 구체적인 설명을 위해 위의 CSSOM 트리를 예로 들어보세요. body 요소 내부의 span 태그 내에 배치된 모든 텍스트는 16픽셀의 글꼴 크기와 빨간색을 갖습니다. 글꼴 크기 지시문은 본문에서 범위까지 계단식으로 적용됩니다. 그러나 범위 태그가 단락(p) 태그의 하위 태그인 경우 해당 내용은 표시되지 않습니다.
또한 위의 트리는 완전한 CSSOM 트리가 아니며 스타일시트에서 재정의하기로 결정한 스타일만 표시합니다. 모든 브라우저는 기본 스타일 세트("사용자 에이전트 스타일"이라고도 함)를 제공합니다. 이러한 기본 스타일을 재정의하면 됩니다.
CSS 처리에 걸리는 시간을 이해하려면 DevTools에서 타임라인을 기록하고 "스타일 다시 계산" 이벤트를 찾으세요. DOM 구문 분석과 달리 타임라인에는 별도의 "CSS 구문 분석" 항목이 표시되지 않고 대신 구문 분석 및 CSSOM 트리가 캡처됩니다.
우리의 작은 스타일시트는 처리하는 데 약 0.6밀리초가 걸리며 페이지의 8개 요소에 영향을 미칩니다. 많지는 않지만 여전히 오버헤드가 발생합니다. 그런데 이 8가지 요소는 어디서 오는 걸까요? DOM과 CSSOM을 함께 묶는 것은 렌더링 트리입니다.
CSSOM 트리와 DOM 트리는 렌더링 트리로 병합된 다음 표시되는 각 요소의 레이아웃을 계산하고 이를 드로잉 프로세스로 출력하여 픽셀을 화면에 렌더링하는 데 사용됩니다. . 최적의 렌더링 성능을 달성하려면 이러한 각 단계를 최적화하는 것이 중요합니다.
브라우저는 HTML 및 CSS 입력을 기반으로 DOM 트리와 CSSOM 트리를 구축합니다. 그러나 이들은 완전히 독립적인 개체로서 문서의 다양한 측면을 포착합니다. 하나는 콘텐츠를 설명하고 다른 하나는 문서에 적용해야 하는 스타일 규칙을 설명합니다. 이 둘을 어떻게 병합하고 브라우저가 화면에 픽셀을 렌더링하도록 합니까?
DOM 트리는 CSSOM 트리와 병합되어 웹 페이지를 렌더링하는 데 필요한 노드만 포함하는 렌더링 트리를 형성합니다. 각 DOM 트리의 노드 노드를 탐색하고 CSSOM 규칙 트리에서 현재 노드의 스타일을 찾아 렌더링 트리를 생성합니다.
레이아웃은 각 개체의 정확한 위치와 크기를 계산합니다.
마지막 단계는 최종 렌더링 트리를 사용하여 픽셀을 화면에 렌더링하는 그리기입니다.
첫 번째 단계는 브라우저가 DOM과 CSSOM을 "렌더링 트리"로 병합하여 웹 페이지의 모든 visible DOM content과 모든 CSSOM 스타일 정보를 포함하도록 하는 것입니다. 각 노드의 .
렌더링 트리를 구축하기 위해 브라우저는 일반적으로 다음 작업을 완료합니다.
DOM 트리의 루트 노드부터 시작하여 표시되는 각 노드를 탐색합니다.
일부 노드는 표시되지 않으며(예: 스크립트 태그, 메타 태그 등) 렌더링된 출력에 반영되지 않기 때문에 무시됩니다.
일부 노드는 CSS를 통해 숨겨져 있으므로 렌더링 트리에서 무시됩니다. 예를 들어, "display: none" 속성은 스팬 노드에 설정되어 있으므로 렌더링 트리에 표시되지 않습니다.
遍历每个可见节点,为其找到适配的 CSSOM 规则并应用它们。从选择器的右边往左边开始匹配,也就是从CSSOM树的子节点开始往父节点匹配。
Emit visible nodes with content and their computed styles.
注: visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分。
最终输出的渲染同时包含了屏幕上的所有可见内容及其样式信息。有了渲染树,我们就可以进入“布局”阶段。
到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们尚未计算它们在设备视口内的确切位置和大小---这就是“布局”阶段,也称为“reflow”。
为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。让我们考虑一个简单的实例:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><title>Critial Path: Hello world!</title> </head> <body><div style="width: 50%"> <div style="width: 50%">Hello world!</div></div> </body></html>
위 웹페이지의 본문에는 두 개의 중첩된 div가 포함되어 있습니다. 첫 번째(상위) div는 노드의 표시 크기를 뷰포트 너비의 50%로 설정하고, 상위 div에 포함된 두 번째 div의 너비는 뷰포트 너비의 50%입니다. 해당 부모는 뷰포트 너비의 25%입니다.
레이아웃 프로세스의 출력은 뷰포트 내 각 요소의 정확한 위치와 크기를 정확하게 캡처하는 "상자 모델"입니다. 모든 상대적 측정값은 화면의 절대 픽셀로 변환됩니다.
마지막으로 어떤 노드가 표시되는지, 계산된 스타일 및 형상 정보가 있는지 알았으므로 이 정보를 최종 단계인 렌더 트리의 각 노드를 화면의 실제 픽셀로 변환하는 단계로 전달할 수 있습니다. 이 단계를 흔히 "페인팅" 또는 "래스터화"라고 합니다.
Chrome DevTools는 위에 언급된 세 단계 모두에서 시간 소모를 심층적으로 이해하는 데 도움이 됩니다. 원래 "hello world" 예제의 레이아웃 단계를 살펴보겠습니다.
"Layout" 이벤트는 타임라인에서 렌더 트리 구성, 위치 및 크기 계산을 캡처합니다.
레이아웃이 완료되면 브라우저 문제는 렌더링 트리를 화면의 픽셀로 변환하는 "페인트 설정" 및 "페인트" 이벤트입니다.
렌더 트리 구성, 레이아웃 및 페인팅을 수행하는 데 필요한 시간은 문서 크기, 적용된 스타일 및 문서가 실행되는 장치: 문서가 클수록 브라우저가 수행해야 하는 작업이 많아지고 스타일이 복잡할수록 그리는 데 시간이 더 걸립니다. 예를 들어 단일 색상은 그리는 데 "더 작아집니다". 그림자는 많은 것을 계산하고 렌더링하는 데 "훨씬 더 비쌉니다").
다음은 브라우저에서 수행되는 단계에 대한 간략한 개요입니다.
HTML 마크업을 처리하고 DOM 트리를 구축합니다.
CSS 마크업을 처리하고 CSSOM 트리를 구축하세요.
DOM과 CSSOM을 렌더 트리에 병합합니다.
렌더링 트리에 따라 배치하여 각 노드의 기하학적 정보를 계산합니다.
각 노드를 화면에 그립니다.
DOM 또는 CSSOM이 수정된 경우 위의 모든 단계를 다시 수행하여 화면에 다시 렌더링해야 하는 픽셀을 결정해야 합니다.
주요 렌더링 경로를 최적화하는 것은 위 순서에서 1~5단계를 수행하는 데 소요되는 총 시간을 최소화하는 프로세스입니다. 이렇게 하면 콘텐츠가 화면에 최대한 빨리 렌더링되고 그 사이의 시간도 줄어듭니다. 즉, 대화형 콘텐츠에 대해 더 높은 새로 고침 비율을 달성합니다.
기본적으로 CSS는 렌더링을 차단하는 리소스로 처리됩니다(그러나 html 구문 분석은 차단하지 않습니다). ) 이는 CSSOM이 빌드될 때까지 브라우저가 처리된 콘텐츠를 렌더링하지 않음을 의미합니다. CSS를 최소화하고, 가능한 한 빨리 제공하고, 미디어 유형과 쿼리를 활용하여 렌더링 차단을 해제하여 스크롤 없이 볼 수 있는 부분에 걸리는 시간을 줄이세요.
렌더링 트리 구성에서는 렌더링 트리를 구성하기 위해 DOM과 CSSOM이 모두 필요합니다. 이는 성능에 심각한 영향을 미칠 수 있습니다. HTML 및 CSS 은 모두 렌더링 차단 리소스입니다. DOM이 없으면 렌더링할 것이 없기 때문에 HTML은 분명히 필요하지만 CSS의 필요성은 덜 명확할 수 있습니다. CSS 렌더링을 차단하지 않고 일반 웹페이지를 렌더링하려고 하면 어떻게 되나요?
기본적으로 CSS는 렌더링 차단 리소스로 간주됩니다.
미디어 유형 및 미디어 쿼리를 통해 일부 CSS 리소스를 비렌더링 차단으로 표시할 수 있습니다.
브라우저는 차단 여부에 관계없이 모든 CSS 리소스를 다운로드합니다.
CSS가 없는 웹페이지는 사실상 사용할 수 없습니다. 따라서 브라우저는 DOM과 CSSOM이 모두 준비될 때까지 렌더링을 차단합니다.
CSS 은 렌더링 차단 리소스입니다. 첫 번째 렌더링 시간을 단축하려면 최대한 빨리 클라이언트에 다운로드해야 합니다.
웹페이지를 표시하거나 대형 모니터에 투사할 때 등 특정 조건에서만 사용되는 CSS 스타일이 있다면 어떨까요? 이러한 리소스가 렌더링을 차단하지 않는다면 좋을 것입니다.
이러한 상황은 CSS "미디어 유형" 및 "미디어 쿼리"를 통해 해결할 수 있습니다.
媒体查询由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。例如,第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况。也就是说它始终会阻塞渲染。第二个样式表则不然,它只在打印内容时适用---或许您想重新安排布局、更改字体等等,因此在网页首次加载时,该样式表不需要阻塞渲染。最后一个样式表声明提供了由浏览器执行的“媒体查询”:符合条件时,样式表会生效,浏览器将阻塞渲染,直至样式表下载并处理完毕。
通过使用媒体查询,我们可以根据特定用例(比如显示或打印),也可以根据动态情况(比如屏幕方向变化、尺寸调整事件等)定制外观。声明样式表时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。
让我们考虑下面这些实例:
第一个声明阻塞渲染,适用于所有情况。
第二个声明同样阻塞渲染:“all”是默认类型,和第一个声明实际上是等效的。
第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。
最后一个声明只在打印网页时应用,因此网页在浏览器中加载时,不会阻塞渲染。
最后,“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论媒寻是否命中,浏览器都会下载上述所有的CSS样式表,只不过不阻塞渲染的资源对当前媒体不生效罢了。
JavaScript 允许我们修改网页的方方面面:内容、样式以及它如何响应用户交互。不过,JavaScript 也会阻止 DOM 构建和延缓网页渲染。为了实现最佳性能,可以让 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。
JavaScript 可以查询和修改 DOM 与 CSSOM。
JavaScript的 执行会阻止 CSSOM的构建,所以和CSSOM的构建是互斥的。
JavaScript blocks DOM construction unless explicitly declared as async.
JavaScript 是一种运行在浏览器中的动态语言,它允许对网页行为的几乎每一个方面进行修改:可以通过在 DOM 树中添加和移除元素来修改内容;可以修改每个元素的 CSSOM 属性;可以处理用户输入等等。为进行说明,让我们用一个简单的内联脚本对之前的“Hello World”示例进行扩展:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script</title><style> body { font-size: 16px };p { font-weight: bold }; span { color: red };p span { display: none }; img { float: right }</style> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script> var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text content span.style.display = 'inline'; // change CSSOM property // create a new element, style it, and append it to the DOM var loadTime = document.createElement('div'); loadTime.textContent = 'You loaded this page on: ' + new Date(); loadTime.style.color = 'blue'; document.body.appendChild(loadTime);</script> </body></html>
JavaScript 允许我们进入 DOM 并获取对隐藏的 span 节点的引用 -- 该节点可能未出现在渲染树中,却仍然存在于 DOM 内。然后,在获得引用后,就可以更改其文本,并将 display 样式属性从“none”替换为“inline”。现在,页面显示“Hello interactive students!”。
JavaScript 还允许我们在 DOM 中创建、样式化、追加和移除新元素。从技术上讲,整个页面可以是一个大的 JavaScript 文件,此文件逐一创建元素并对其进行样式化。但是在实践中,使用 HTML 和 CSS 要简单得多。
尽管 JavaScript 为我们带来了许多功能,不过也在页面渲染方式和时间方面施加了更多限制。
首先,请注意上例中的内联脚本靠近网页底部。为什么呢?如果我们将脚本移至 span元素前面,就会脚本运行失败,并提示在文档中找不到对任何span 元素的引用 -- 即 getElementsByTagName(‘span') 会返回 null。这透露出一个重要事实:脚本在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。
换言之,我们的脚本块在运行时找不到网页中任何靠后的元素,因为它们尚未被处理!或者说:执行内联脚本会阻止 DOM 构建,也就延缓了首次渲染。
在网页中引入脚本的另一个微妙事实是,它们不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。实际上,示例中就是这么做的:将 span 元素的 display 属性从 none 更改为 inline。最终结果如何?我们现在遇到了race condition(资源竞争)。
如果浏览器尚未完成 CSSOM 的下载和构建,而却想在此时运行脚本,会怎样?答案很简单,对性能不利:浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。
简言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了大量新的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟:
脚本在文档中的位置很重要。
当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
JavaScript 可以查询和修改 DOM 与 CSSOM。
JavaScript 执行将暂停,直至 CSSOM 就绪。即CSSDOM构建的优先级更高。
“优化关键渲染路径”在很大程度上是指了解和优化 HTML、CSS 和 JavaScript 之间的依赖关系谱。
默认情况下,JavaScript 执行会“阻塞解析器”:当浏览器遇到文档中的脚本时,它必须暂停 DOM 构建,将控制权移交给 JavaScript 运行时,让脚本执行完毕,然后再继续构建 DOM。实际上,内联脚本始终会阻止解析器,除非编写额外代码来推迟它们的执行。
通过 script 标签引入的脚本又怎样:
<html> <head><meta name="viewport" content="width=device-width,initial-scale=1"><link href="style.css?1.1.11" rel="stylesheet"><title>Critical Path: Script External</title> </head> <body><p>Hello <span>web performance</span> students!</p><div><img src="awesome-photo.jpg"></div><script src="app.js?1.1.11"></script> </body></html>
app.js
var span = document.getElementsByTagName('span')[0]; span.textContent = 'interactive'; // change DOM text contentspan.style.display = 'inline'; // change CSSOM property// create a new element, style it, and append it to the DOMvar loadTime = document.createElement('div'); loadTime.textContent = 'You loaded this page on: ' + new Date(); loadTime.style.color = 'blue'; document.body.appendChild(loadTime);
无论我们使用