浏览器的底层渲染机制
[线程 & 进程]
进程 浏览器打开一个页面是开辟一个进程,
线程 在这个进程中还会做一些事情,这些事情交给线程来做,浏览器是多线程的:这样可以同时做很多事情
一个进程中包含多个线程
- ,每个线程同时只能做一件事,这件事处理完成才能做下一件事=>同步编程,同时只能处理一件事情,上一件事情处理完才能做下一件事
- 异步编程:多线程,同时可以处理多件事情
浏览器具备的线程
- + GUI渲染线程 「自上而下解析渲染HTML/CSS代码」 HTML CSS IMG W3C规范
- + JS引擎线程 「解析和渲染JS代码」 JavasScript官方规范
- + HTTP网络线程 「从服务器获取资源和数据的」最多可以开辟5-7个
- + 定时器的监听线程(监听定时器是否到达时间)
- + 事件监听线程(监听事件是否触发)
- + ...
浏览器只会分配一个“JS引擎线程”去渲染JS,所以JS是单线程的
浏览器是如何渲染页面的
当我们从服务器获取HTML代码后,浏览器会按照自己的代码规则,绘制出对应的界面
- @1 生成DOM TREE「描述DOM/节点的层级关系」
- 生成CSSOM TREE「等待样式资源获取到,GUI会进行渲染,把样式按照“层叠样式表”渲染为CSSOM TREE」一般发生在DomTREE之后
- @3 把DOM TREE 和 CSSOM TREE结合在一起,生成RENDER TREE「规划出每个节点应该渲染的样式,后期渲染页面就是按照这个来的」
- @4 Layout布局「根据视口大小,计算每个节点在视口中的位置和大小等;如果后期的某些操作会导致视口中的节点位置、大小或者视口大小发生改变,此时我们需要重新计算每个节点在视口中的最新位置等,我们把这个操作叫做“回流/重排 Reflow”」
- @5 分层「按照样式,分成不同的层级(文档流),并计算出每一层文档流中的节点如何渲染」
- @6 Painting绘制「绘制完成的结果就是在浏览器中看到页面和效果;如果后期某些节点的样式发生改变(位置和大小等不变,只是样色、背景等样式改变),浏览器需要重新按照最新的样式去绘制,我们这个操作称之为“重绘 Repaint”」
- 页面第一次渲染一定会引发一次Layout&Painting;后期发生重排一定也会引发重绘;但是重绘不一定非要重排;重排非常的消耗性能(这也是我们所谓操作DOM损耗性能的主要原因)!
关于样式
GUI线程自上而下渲染代码
- + 遇到link/img...,会开辟新的HTTP线程去获取资源,GUI渲染线程继续向下执行「异步的」
- + 遇到<style>,资源已经存在无需去服务器获取,但是需要等待其它外链资源获取回来后,GUI按照导入的先后顺序依次渲染CSS样式,以此保证样式的优先级!!
- + 遇到@import,也会开辟新的HTTP线程去获取资源,但是会阻碍GUI的渲染,什么时候资源获取到,什么时候GUI才会继续向下执行!「同步的」
- + 遇到<script>也是开辟新的HTTP线程去获取资源,此时GUI停止渲染,当资源获取到之后,JS引擎线程先去渲染JS,等到JS渲染完毕后,GUI再继续... 「同步的」
+ 我们一般都把JS放在页面底部「作用:不让其阻碍GUI的渲染;等待DOM渲染完再去执行JS,保证JS中可以正常的操作DOM;」
+ 如果不放在底部,还想获取DOM
- + 办法一:基于DOMContentLoaded事件监听处理「本质:script的获取和渲染还是同步的,还会阻碍GUI,只不过我们把操作DOM的代码放在事件监听中延后执行了」
- + 办法二:给script设置async或者defer属性「本质:把script的获取和渲染改为异步操作,不让其阻碍GUI的渲染」
- + async:遇到script分配一个HTTP线程去获取资源,此时GUI继续渲染,但是当资源获取到之后,立即停止GUI的渲染,先把获取的JS渲染...「不关注JS导入顺序」
- + defer:和link很相似,获取资源和代码的渲染都是异步的,都需要等到GUI渲染完,而且JS资源也都获取到了,按照JS的先后导入顺序,依次渲染解析JS「关注JS的相互依赖」
- 如果没有JS之间的相互依赖,完全可以谁先回来执行谁,使用async即可:但凡需要数据导入的先后顺序去执行才可以,则一定要用defer
项目(前端)性能优化方案
针对于关键渲染路径的优化
@1 不同的资源分服务器部署「优势:资源合理利用、提高服务器处理能力、提高HTTP的并发数 弊端:增加了DNS解析的次数」,所以在这个基础上,我们使用DNS Prefetch预解析「原理:利用link的异步性,让GUI渲染的同时,也去预先解析DNS,后面再获取资源的时候,直接把DNS缓存的信息拿出来用即可...」
@2 减少操作DOM产生重排和重绘
- + 放弃直接操作DOM,我们主攻操作数据,把操作DOM的事情交给vue/react框架来完成「框架内部做了很多操作DOM的优化」:虚拟DOM->DOM DIFF->渲染差异内容
- + 利用浏览器的渲染队列机制,我们操作DOM的样式进行“读写分离「集中修改样式」”
- + 动态创建多个DOM节点,我们基于文档碎片或者模板字符串,实现批量增加
- + 基于transform修改元素的样式,不会引发重排
- + 尽量操作在单独文档流中的节点,这样节点位置或大小改变,只会把这一层中的节点重新Layout,虽然也引发了重排,但是总比所有节点都重新计算位置强...
@3 对于静态资源文件设置强缓存和协商缓存,目的是保证第二次及以后加载页面更快
@4 基于CDN做资源分布式部署
@5 降低HTML嵌套的层级、使用语义化标签
@6 保持TCP通道的长链接 Connection:keep-alive
@7 尽可能使用HTTP2.0
@8 对于script标签来讲,尽可能放在页面末尾导入,如果非要放在前面导入,需要加async/defer,避免对GUI阻塞
@9 如果样式资源比较少,直接内嵌式即可,减少一次HTTP请求;如果内容多,则合并到一个css中,使用外链式link导入;坚决不用@import导入式,因为他会阻塞GUI渲染!!
@10 使用本地存储方案,对不经常更新的数据做数据缓存
@11 link导入样式的操作放在HEAD中,让GUI渲染DOM TREE的同时,也去请求CSS资源,这样等到DOM TREE完成,可能CSS资源已经拿回来了
@12 开启服务器端的GZIP压缩「让每一次返回的资源压缩40%+」
@13 合理使用图片base64
- + 需要基于webpack自己编译BASE64「不要自己手动写BASE64,因为代码太恶了」
- + 零散的小图片一般可以base64
- + 如果这个图片很大,并且还很重要(不能延迟加载),如果想尽各种办法加载还是慢,不妨使用BASE64
====针对于提高页面第一次打开速度的优化{减少白屏等待时间}====
@1 图片懒加载「第一次渲染页面不去加载真实图片(页面中基于默认图占位):减少了HTTP请求次数、不占用HTTP并发资源、第一次加载页面也无需渲染图片... 让页面第一次加载更快」
@2 骨架屏
- + 服务器骨架屏(SSR渲染):页面首屏需要展示的结构、样式、数据等都由服务器处理好,第一次加载页面,只要获取到内容,直接渲染即可(真实数据也有了) -> 前提服务器抗压能力需要好
- + 前端骨架屏:渲染之前的Loading效果;在真实内容没有渲染出来之前,先把架子搭起来,用一些灰色的框框占位,给用户正在加载的友好效果...
@3 减少HTTP的请求次数和大小「因为HTTP的并发性、TCP的三握四挥、网络通道可能会被阻塞等众多原因,决定了HTTP请求次数越少越好」
+ CSS和JS资源合并为一个「如果一个文件过大,第一次加载页面不需要这么多东西,我们也可以切割成多个,但是第一次只加载一个必须的,其余的动态异步加载」
- + 使用CSS Sprite技术,多张图片合并为一个
- + 文件要压缩,图片资源在保证清晰度的前提下,尽可能压缩
- + 使用字体图标/SVG(矢量图)代替位图(jpg/png/gif...
@4 音视频资源一定要做延迟加载和播放
@5 数据的分页处理、异步加载、下拉刷新...
====运行时的代码优化====
@1 基于事件委托处理事件绑定「优势:减少堆栈内存的开辟、可以给动态创建的元素做事件绑定...提高了整体性能」
@2 减少cookie的使用「因为每一次向服务器发送请求,都会在请求头中把cookie传递给服务器,不论服务器是否想要,如果本地cookie存储信息多,则每次传输都会携带一些没必要的内容...」
@3 对于一些操作应该使用函数的防抖和节流
@4 CSS选择器前缀不要过长「CSS选择器的渲染方向:右->左」
@5 合理使用闭包「闭包会产生不释放的栈内存」
@6 避免出现死递归(因为会导致栈溢出)、避免出现死循环(因为会阻塞JS引擎线程的渲染)
@7 动画处理的原则:能用CSS搞定的不用JS,能用requestAnimationFrame搞定的不用定时器,如果最后定时器动画都搞不定的,换需求...
@8 避免内存泄漏 -> javascript高级程序设计第三版
@9 避免使用CSS的表达式{expression}
webpack层面:优化打包部署
vue/react层面