浏览器解析渲染HTML
解析流程
[浏览器层合成与页面渲染优化] juejin.cn/post/684490…
一般可以分为 构建 DOM 树
、构建渲染树
、布局
、绘制
、渲染层合成
几个步骤
构建 DOM 树:浏览器将 HTML 解析成树形结构的 DOM 树,一般来说,这个过程发生在页面初次加载,或页面 JavaScript 修改了节点结构的时候。
构建渲染树:浏览器将 CSS 解析成树形结构的 CSSOM 树,再和 DOM 树合并成渲染树。
布局(Layout) :浏览器根据渲染树所体现的节点、各个节点的CSS定义以及它们的从属关系,计算出每个节点在屏幕中的位置。Web 页面中元素的布局是相对的,在页面元素位置、大小发生变化,往往会导致其他节点联动,需要重新计算布局,这时候的布局过程一般被称为回流(Reflow)。
绘制(Paint) :遍历渲染树,调用渲染器的 paint()
方法在屏幕上绘制出节点内容,本质上是一个像素填充的过程。这个过程也出现于回流或一些不影响布局的 CSS 修改引起的屏幕局部重画,这时候它被称为重绘(Repaint)。实际上,绘制过程是在多个层上完成的,这些层我们称为渲染层(RenderLayer)。
渲染层合成(Composite) :多个绘制后的渲染层按照恰当的重叠顺序进行合并,而后生成位图,最终通过显卡展示到屏幕上。
这是一个基本的浏览器从解析到绘制一个 Web 页面的过程,跟上边页面卡顿问题的解决方法相关的,主要是最后一个环节 —— 渲染层合成。
合成层(CompositingLayer)
满足某些特殊条件的渲染层,会被浏览器自动提升为合成层。合成层拥有单独的 GraphicsLayer,而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 的父层共用一个。
那么一个渲染层满足哪些特殊条件时,才能被提升为合成层呢?这里列举了一些常见的情况:
- 3D transforms:translate3d、translateZ 等
- video、canvas、iframe 等元素
- 通过 Element.animate() 实现的 opacity 动画转换
- 通过 СSS 动画实现的 opacity 动画转换
- position: fixed
- 具有 will-change 属性
- 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition
因此,文首例子的解决方案,其实就是利用 will-change 属性,将 CPU 消耗高的渲染元素提升为一个新的合成层,才能开启 GPU 加速的,因此你也可以使用 transform: translateZ(0)
来解决这个问题。
这里值得注意的是,不少人会将这些合成层的条件和渲染层产生的条件混淆,这两种条件发生在两个不同的层处理环节,是完全不一样的。
隐式合成
上边提到,满足某些显性的特殊条件时,渲染层会被浏览器提升为合成层。除此之外,在浏览器的 Composite 阶段,还存在一种隐式合成,部分渲染层在一些特定场景下,会被默认提升为合成层。
- 两个 absolute 定位的 div 在屏幕上交叠了,根据
z-index
的关系,其中一个 div 就会”盖在“了另外一个上边。
- 这个时候,如果处于下方的 div 被加上了 CSS 属性:
transform: translateZ(0)
,就会被浏览器提升为合成层。提升后的合成层位于 Document 上方,假如没有隐式合成,原本应该处于上方的 div 就依然还是跟 Document 共用一个 GraphicsLayer,层级反而降了,就出现了元素交叠关系错乱的问题。
- 所以为了纠正错误的交叠顺序,浏览器必须让原本应该”盖在“它上边的渲染层也同时提升为合成层。
浏览器解析渲染页面
- 解析HTML形成DOM树
- 解析CSS形成CSSOM 树
- 合并DOM树和CSSOM树形成渲染树
- 浏览器开始渲染并绘制页面
渲染Layout(布局) 过程涉及两个比较重要的概念回流和重绘
DOM结点都是以盒模型形式存在,需要浏览器去计算位置和宽度等,这个过程就是回流。
等到页面的宽高,大小,颜色等属性确定下来后,浏览器开始绘制内容,这个过程叫做重绘。
然后就是浏览器渲染,渲染的主要过程分为——Render Tree(渲染树)生成——Layout(布局)——Paint(绘制)
回流(重排)
因为浏览器渲染是一个由上而下的过程,当发现某部分的变化影响了布局时,就需要倒回去重新渲染,这个过程就称之为回流。reflow几乎是没法避免的,一些常用的效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。
会导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如::hover)
- 查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
重绘
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
优化
通常重排比重绘会耗费更多的时间,从而也就会影响性能,所以编写代码的时候要尽可能避免过多的重排或者重绘。
-
修改样式不要逐条修改,建议定义CSS样式的class,然后直接修改元素的className
-
不要将DOM节点的属性值放在循环中当成循环的变量
-
为动画的 HTML 元素使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的
-
尽可能在DOM树的最末端改变class
-
避免设置多层内联样式
-
避免使用CSS表达式(例如:calc())
-
不要使用table布局,一个微小的改变就可能引起整个table的重新布局
-
编写合理的CSS, CSS选择符的匹配顺序,从右到左!从右到左!从右到左!(重要的事情说三遍),所以,类似于“#nav li” 我们以为很简单的规则,应该马上就可以匹配成功,但是,需要从右往左匹配,所以,先会去查找所有的li,然后再去确定它的父元素是不是#nav。因此,编写合理的CSS也可以提高我们的页面行能:
- DOM的深度尽量浅,不要嵌套过深。
- 减少inline javascript css的数量。
- 不要为ID选择器指定类名或者标签名。
- 避免后代选择器,尽量使用子选择器。
- 避免使用通配符。
\
渲染十万条列表
简单型
使用 requestAminationFrament 和 documentFrament
虚拟列表
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域
内需要的列表项,当滚动发生时,动态通过计算获得可视区域
内的列表项,并将非可视区域
内存在的列表项删除。
<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
复制代码
infinite-list-container
为可视区域
的容器infinite-list-phantom
为容器内的占位,高度为总列表高度,用于形成滚动条infinite-list
为列表项的渲染区域
接着,监听infinite-list-container
的scroll
事件,获取滚动位置scrollTop
- 假定
可视区域
高度固定,称之为screenHeight
- 假定
列表每项
高度固定,称之为itemSize
- 假定
列表数据
称之为listData
- 假定
当前滚动位置
称之为scrollTop
则可推算出:
- 列表总高度
listHeight
= listData.length * itemSize - 可显示的列表项数
visibleCount
= Math.ceil(screenHeight / itemSize) - 数据的起始索引
startIndex
= Math.floor(scrollTop / itemSize) - 数据的结束索引
endIndex
= startIndex + visibleCount - 列表显示数据为
visibleData
= listData.slice(startIndex,endIndex)
当滚动后,由于渲染区域
相对于可视区域
已经发生了偏移,此时我需要获取一个偏移量startOffset
,通过样式控制将渲染区域
偏移至可视区域
中。
- 偏移量
startOffset
= scrollTop - (scrollTop % itemSize);
<template>
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
>{{ item.value }}</div>
</div>
</div>
</template>
复制代码
export default {
name:'VirtualList',
props: {
//所有列表数据
listData:{
type:Array,
default:()=>[]
},
//每项高度
itemSize: {
type: Number,
default:200
}
},
computed:{
//列表总高度
listHeight(){
return this.listData.length * this.itemSize;
},
//可显示的列表项数
visibleCount(){
return Math.ceil(this.screenHeight / this.itemSize)
},
//偏移量对应的style
getTransform(){
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取真实显示列表数据
visibleData(){
return this.listData.slice(this.start, Math.min(this.end,this.listData.length));
}
},
mounted() {
this.screenHeight = this.$el.clientHeight;
this.start = 0;
this.end = this.start + this.visibleCount;
},
data() {
return {
//可视区域高度
screenHeight:0,
//偏移量
startOffset:0,
//起始索引
start:0,
//结束索引
end:null,
};
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemSize);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemSize);
}
}
};