阅读 347

浏览器渲染相关

浏览器渲染页面的机制和原理

现在操作系统比如Mac OS X,UNIX, Linux, Windows等,都是支持“多任务”的操作系统。

  • 单核CPU执行多任务:操作系统轮流让各个任务交替执行,由于CPU执行速度很快,所以我们感觉就像所有任务都在同时执行一样。

  • 多核CPU执行多任务:真正的并行执行多任务只能在多核CPU上实现,但是由于任务数量远远多于多核CPU的核心数量,所以操作系统还是会自动把很多任务轮流调度到每个核心上执行。

有些进程可能不止同时干一件事,在干多件事的情况下,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程

多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替进行,看起来就像同时执行一样。

DOM的回流与重绘

  • 重绘:元素样式的改变(宽高,大小,位置不变)

  • 回流:元素大小或位置发生变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染

ps:因为回流是根据视口的大小来计算元素的位置和大小的,所以浏览器的窗口尺寸变化也会引发回流。

  • 回流一定会触发重绘,重绘不一定会触发回流。

优化DOM交互

  • 使用文档碎片减少DOM交互次数。DOM交互越多,性能越慢。
  var list = document.getElementById("myList"),
      item,
      i;
 
  for (i = 0; i <= 10; i++) {
       item.document.createElement("li");
       list.appendChild(item);
       item.appendChild(document.createTextNode(" Item" + i));
  }
复制代码

上面代码每执行一次for循环都会向DOM插入新的元素,一旦for循环次数很多,那么严重影响代码性能,解决办法就是减少DOM交互。

可以使用createDocumentFragment方法创建虚拟节点,把要插入DOM的元素先插入该虚拟节点,循环完之后再把虚拟节点插入DOM,虚拟节点是不会渲染出来的,只会渲染它的子节点。改进代码如下:

  var list = document.getElementById("myList");
        fragment = document.createDocumentFragment(),
        i;
 
  for (i = 0; i < 10; i++) {
       item = document.createElement("li");
       fragment.appendChild(item);
       item.appendChild(document.createTextNode("Item" + i));
  }
  
  list.appendChild(fragment);
复制代码

当我们要批量修改DOM节点的时候,可以将DOM节点隐藏掉,然后进行一系列的修改操作,之后再将其设置为可见,这样就可以最多只进行两次重排。具体的方法如下:

  // 未优化前
  const ele = document.getElementById('test');
  // 一系列dom修改操作

  // 优化方案一,将要修改的节点设置为不显示,之后对它进行修改,修改完成后再•显示该节点,从而只需要两次重排
  const ele = document.getElementById('test');
  ele.style.display = 'none';
  // 一系列dom修改操作
  ele.style.display = 'block';

  // 优化方案二,首先创建一个文档片段(documentFragment),然后对该片段进行修改,之后将文档片段插入到文档中,只有最后将文档片段插入文档的时候会引起重排,因此只会触发一次重排。。
  const fragment = document.createDocumentFragment();
  const ele = document.getElementById('test');
  // 一系列dom修改操作
  ele.appendChild(fragment);
复制代码
  • 使用innerHTML

有两种在页面上创建DOM节点的方法:诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用,由于内部方式是编译好的而非解释执行的,所以执行快的多。

  • 使用事件委托

把事件绑定在祖先节点,由于有事件冒泡,当事件触发时根据event对象的target属性可以知道具体事件是在那个子元素发生的。从而执行不同的行为。这样就不必每个子节点都绑定事件。

假设你有一个列表,里面每一个列表项都需要绑定相同的事件,而这个列表可能会频繁的插入和删除。如果按照平常的方法,你只能给每一个列表项都绑定一个事件处理器,并且,每当插入新的列表项的时候,你也需要为新的列表项注册新的事件处理器。这样的话,如果列表项很大的话,就会导致有特别多的事件处理器,造成极大的性能问题。而通过事件委托,我们只需要在列表项的父节点监听这个事件,由它来统一处理就可以了。这样,对于新增的列表项也不需要做额外的处理。而且事件委托的用法其实也很简单:

function handleClick(target) {
  // 点击列表项的处理事件
}
function delegate (e) {
  // 判断目标对象是否为列表项
  if (e.target.nodeName === 'LI') {
    handleClick(e.target);
  }
}
const parent = document.getElementById('parent');
parent.addEventListener('click', delegate);
复制代码
  • css硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们甚至可以不要回流重绘,css3的 transfrom / opacity / filters / ...这些属性会触发硬件加速,但不会引发回流和重绘,不过可能会导致过多使用后大量占用内存,性能消耗严重,字体模糊等。

  • 动画效果应用到position属性值为absolute或fix的元素上,因为它们脱离文档流。

  • 放弃传统操作dom的模式,采用基于vue/react数据影响视图的模式。

  • 将经常访问到的DOM结点进行缓存

访问 DOM 会很慢。如果要多次读取某元素的内容,最好将其保存在局部变量中。但记住重要的是,如果稍后你会删除 DOM 的值,则应将变量设置为“null”,不然会导致内存泄漏。

  • 分离读写操作

offsetTop、offsetLeft、clientTop、clientLeft、scrollWidth、scrollHeight、getComputedStyle...会刷新渲染队列。

拓展

link和@import区别

本质上,这两种方式都是为了加载css文件。

结论

强烈建议使用link标签,慎用@import方式。 这样可以避免考虑@import的语法规则和注意事项,避免产生资源文件下载顺序混乱和http请求过多的烦恼。

区别

1.从属关系

@import是 CSS 提供的语法规则,只有导入样式表的作用;link是HTML提供的标签,不仅可以加载 CSS 文件,还可以定义 RSS、rel 连接属性等。

2.加载顺序

加载页面时,link标签引入的 CSS 被同时加载;@import引入的 CSS 将在页面加载完毕后被加载。

3.兼容性区别

@import是 CSS2.1 才有的语法,故只可在 IE5+ 才能识别;link标签作为 HTML 元素,不存在兼容性问题。

4.DOM可控性区别

可以通过 JS 操作 DOM ,插入link标签来改变样式;由于 DOM 方法是基于文档的,无法使用@import的方式插入样式。

5.权重区别

CSS 权重优先级顺序简单表示为: !important > 行内样式 > ID > 类、伪类、属性 > 标签名 > 继承 > 通配符

link标签引入的 CSS 文件中使用@import时,相同样式将被该 CSS 文件本身的样式层叠。

细节

在《CSS权威指南》中写道:

@import一定要写在除@charset外的其他任何 CSS 规则之前,如果置于其它位置将会被浏览器忽略,而且,在@import之后如果存在其它样式,则@import之后的分号是必须书写,不可省略的。

到此为止,似乎事情都弄清楚了,但是突然又有个疑点浮现出来:

在讨论区别的时候,不是说加载页面时,link标签引入的 CSS 先于@import引入的 CSS 加载吗,那link标签引入的样式又怎会把@import引入的样式层叠掉呢?

要回答这个问题,首先我们要一起明确一些有关浏览器的概念:

浏览器执行过程可以简单分为加载、解析、渲染,这三个步骤。

加载:根据请求的URL进行域名解析,向服务器发送请求,接收响应文件(如 HTML、JS、CSS、图片等)。

解析:对加载到的资源(HTML、JS、CSS等)进行语法解析,构建相应的内部数据结构(比如HTML的DOM树,JS对象的属性表,CSS的样式规则等)。

渲染:构建渲染树,对各个元素进行位置计算、样式计算等,然后根据渲染树完成页面布局及绘制的过程(可以理解为“画”页面元素)。

这几个过程不是完全孤立的,会有交叉,比如HTML加载后就会进行解析,然后拉取HTML中指定的CSS、JS等。

现在,我们应该已经了解了加载和渲染的概念,明白它们是两个不同的过程,那么对上文中抛出的疑问继续追问:

link先于@import加载,是不是也先于@import渲染呢?

实际上,渲染的动作一般都会执行多次,最后一次渲染,一定是依据之前加载过的所有样式整合后的渲染树进行绘制页面的,已经被渲染过的页面元素,也会被重新渲染。

那么我们就可以把@import这种导入 CSS 文件的方式理解成一种替换,CSS 解析引擎在对一个 CSS 文件进行解析时,如在文件顶部遇到@import,将被替换为该@import导入的 CSS 文件中的全部样式。

@import虽然后被加载,却会在加载完毕后置于样式表顶部,最终渲染时自然会被下面的同名样式层叠。

参考: www.cnblogs.com/my--sunshin…

文章分类
前端
文章标签