浏览器基础知识
浏览器内核分类:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari);
浏览器内核组成:渲染引擎 & JS引擎。JS 引擎和渲染引擎是互斥的,同一时间点只能运行一个,所以在执行 JS 时会阻塞页面的渲染;
浏览器有两种呈现模式:doctype(DTD文档类型定义)
- 标准模式:浏览器根据规范呈现页面;
- 混杂模式:页面以一种比较宽松的向后兼容的方式显示;
- DOCTYPE 不存在或格式不正确会导致文档以混杂(兼容)模式呈现。
- 每个标签页都有一个渲染进程,标签页内的所有内容都由渲染进程处理。最新的 Chrome 浏览器尝试为每个站点(域名)提供一个渲染进程。
- 浏览器主进程包含:
- UI 渲染线程
- 网络线程
- Storage thread
浏览器工作流程
DNS解析确定 IP 地址:
- 输入 url 后(由浏览器主进程处理输入的 url),首先需要找到这个 url 域名的服务器 ip ,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录
- DNS解析查找流程:浏览器缓存--->系统缓存--->host文件--->路由器缓存--->ISP(运营商)DNS缓存、根域名服务器、顶级域名服务器、主域名服务器的顺序,逐步读取缓存直到拿到IP地址;
- 性能优化:DNS预解析,
<meta http-equiv="X-dns-fetch-control" content="on">
三次握手建立TCP连接,发送请求:
- 得到服务器的 ip 地址后,浏览器根据这个 ip 以及相应的端口号,构造一个 http 请求,并将这个 http 请求封装在一个 tcp 包中,这个tcp包会依次经过传输层、网络层、数据链路层、物理层到达服务器(可能存在缓存);
服务器收到请求并返回响应,如果响应是 HTML 文件就将数据传递给【渲染进程】;
解析 HTML 和 CSS 构建 DOM 树和 CSS 规则树
- 浏览器根据这个 HTML 来构建 DOM 树,在dom树的构建过程中如果遇到 JS 脚本和外部 JS 连接,则会停止构建 DOM 树来执行和下载相应的代码,这会造成阻塞,这就是为什么推荐JS代码应该放在html代码的后面;
之后根据外部样式,内部样式,内联样式构建一个CSS对象模型树CSSOM树,构建完成后和DOM树合并为渲染树
- 这里主要做的是排除非视觉节点,比如 script,meta 标签和排除 display 为 none 的节点;
之后进行布局,布局主要是确定各个元素的位置和尺寸,
绘制:最后渲染页面,因为html文件中会含有图片、视频、音频等资源,在解析DOM的过程中,遇到这些都会进行并行下载
- 浏览器对每个域的并行下载数量有一定的限制,一般是4-6个
域名解析流程
- 查询浏览器缓存 ---> 操作系统缓存 --->hosts 文件配置 --->本地DNS服务器缓存 --->DNS服务器查询
- DNS服务器查询流程
- 查询根域名服务器
- 查询一级域名服务器
- ...
浏览器渲染过程
页面加载过程:
- 浏览器根据 DNS 服务器得到域名的 IP 地址;
- 建立连接(三次握手),向这个 IP 的机器发送 HTTP 请求;
- 服务器收到、处理并返回 HTTP 请求;
- 浏览器得到返回内容(HTML);
- 页面加载解析顺序:域名解析-->加载HTML-->加载JS、CSS-->加载图像和其他信息
例如:
- 在浏览器输入
https://juejin.im/timeline
,然后经过 DNS 解析,juejin.im对应的 IP 是36.248.217.149
; - 然后浏览器向该 IP 发送 HTTP 请求;
- 服务端接收到 HTTP 请求,然后经过计算,返回 HTTP 请求,返回的内容是HTML、CSS、JS等文件;
- 浏览器得到返回内容;
详情
浏览器有一个固定的渲染流程——只有在布局(layout)完成后才能绘制(paint)页面,而布局的前提是要生成渲染树(render tree),而渲染树的生成则需要 DOM 和 CSSOM 树的配合。
- 关键渲染路径是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。优化关键渲染路径可提高渲染性能。
- 关键渲染路径包含了 文档对象模型(DOM),CSS 对象模型 (CSSOM),渲染树和布局。
- 关键渲染路径共分五个步骤:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制
DOM 构建是增量的,CSSOM 却不是。CSS 是渲染阻塞的:
- 浏览器会阻塞页面渲染直到它接收和执行了所有的 CSS。CSS 是渲染阻塞是因为规则可以被覆盖,所以内容不能被渲染直到 CSSOM 的完成。
(1) 解析文件:
HTML\XHTML\SVG文件解析为------>构建DOM树;HTML 中的 script 如何处理?;
- 遇到外部链接的 CSS 文件和 JS 文件会发起 HTTP请求(预加载扫描器);
- 你可以使用
rel="preload"
将<link>
元素转换为预加载器,用于关键资源,包括 CSS 文件、字体和图片
CSS 文件解析为------>CSS Object Model (CSSOM);
JS 文件加载完成后,执行 JS 代码,操作 DOM 树和 CSS 树;
(2) 文件解析完成后,根据 DOM 树和 CSS 规则树构造渲染树(Rendering Tree):
渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是
display: none
的,那么就不会在渲染树中显示;CSS 规则树是为了完成匹配并把样式应用到 Rendering Tree 上匹配的节点(元素 element )上;
(3) 布局 render 树( Layout/reflow ),计算 Render Tree 中每个元素的位置和尺寸,该过程又称为 layouy 布局或 reflow 回流;若布局完成后对 DOM 进行了修改,将会重新布局(也称回流);
(4) 绘制 render 树( paint ),绘制页面像素信息;
(5) 最后,通过调用操作系统 Native GUI 的 API 完成绘制;
script 和 CSS 是否阻塞
什么是DOM渲染?
DOM 渲染是指浏览器将构建好的 DOM 树与 CSS 样式结合,计算出每个元素的布局,并将其绘制在屏幕上的过程。
渲染过程中如果遇到不带
defer
或async
属性的<script>
就停止渲染,下载并执行 JS 代码。- 因为浏览器有 GUI 渲染线程与 JS 引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。
- 普通的 JS 文件的加载、解析与执行会阻塞 DOM 的构建。在构建 DOM 时,HTML 解析器若遇到了JS,那么它会暂停构建 DOM,将控制权移交给 JS 引擎,等 JS 引擎运行完毕,浏览器再从中断的地方恢复 DOM 构建。
CSSOM 构建完成后才会执行 JS,因此 CSSOM 会阻塞 JS 的运行:
css 下载是异步,下载css时不会阻塞浏览器构建 DOM,但是会阻塞【渲染】。
通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。
因为 JS 不只是可以改 DOM,它还可以更改样式,也就是它可以更改 CSSOM。因为不完整的 CSSOM 是无法使用的,如果JS 想访问 CSSOM 并更改它,那么在执行 JS 时,必须要能拿到完整的 CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JS,最后在继续构建 DOM。
下载CSS 不会阻塞 HTML 的解析,但是它确实会阻塞 JavaScript,因为 JavaScript 经常用于查询元素的 CSS 属性
回流 & 重绘
回流指的是当页面中的元素位置、大小或布局发生变化时,浏览器会重新计算元素的位置和布局信息,并将这些变化应用到页面上,这个过程会涉及到重新布局和重绘页面的一部分或全部内容,比较耗费性能。
重绘是指当元素的样式属性(如颜色、背景色等)发生变化时,浏览器会重新绘制这些元素的样式,但是不会影响到它们的布局,所以比回流性能开销小。
当网页生成的时候,至少会渲染一次,在用户访问的过程中,还会不断重新渲染,重新渲染会重复回流+重绘或者只有重绘。回流一定会引发重绘,重绘不一定引发回流。
回流
- 当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
- JS 对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
- JS 动态修改了 DOM 属性或是 CSS 属性会导致重新 Layout(回流),但有些改变不会重新 Layout,比如修改后的CSS rule没有被匹配到元素。
Q:什么情况下发生回流?什么情况下发生重绘?
- 任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流
- 添加或者删除可见的DOM元素;
- 元素尺寸改变——边距、填充、边框、宽度和高度;
- 内容变化,比如用户在input框中输入文字;
- 浏览器窗口尺寸改变——resize事件发生时;
- 计算 offsetWidth 和 offsetHeight 属性;
- 设置 style 属性的值;
- 改变 top 和 left等属性会触发回流,但使用 transform 属性的 translate 来切换动画只会触发重绘,因为这是基于GPU实现的。
- 改变字体大小会引发回流
- 当获取一些属性时,浏览器为了获得正确的值也会触发回流,包括:
- offset(Top/Left/Width/Height)
- scroll(Top/Left/Width/Height)
- cilent(Top/Left/Width/Height)
- width,height
- 调用了getComputedStyle()或者IE的currentStyle
重绘
JS 对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了回流环节)。
引起重绘的方法:
- 背景色、颜色、字体改变(注意:字体大小发生变化时,会触发回流);
- color、border-style、visibility
如何减少回流和重绘?
在网页开发中,应该尽量避免频繁触发回流和重绘操作,因为它们会导致页面性能下降,影响用户体验。以下是一些减少回流和重绘的方法:
- 减少逐项更改样式,最好一次性更改style,或者将样式定义为class并一次性更新
- 尽量使用 transform 和 opacity 等 CSS 属性来代替 top 和 left 等属性,因为前者不会触发回流。
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局);
- 使用文档片段(DocumentFragment)来创建DOM元素,因为它可以在内存中操作DOM,避免频繁的回流和重绘。
- 避免使用 table 布局,因为它会导致回流的频繁发生。
- 避免多次读取offset等属性。无法避免则将它们缓存到变量
- 避免使用不必要的 DOM 层级,减少 DOM 元素的嵌套,以降低回流和重绘的成本。
- 使用 debounce 和 throttle 等技术来减少事件处理程序的频繁触发,以避免过度的回流和重绘。
相关 CSS 属性:transform
、will-change
、contain
、content-visibility
浏览器中JS的执行过程
延迟脚本: <scipt defer> 异步脚本: <scipt async>
- DOM 在第一次页面加载完毕后,就在内存里了,无论后面怎么通过 ajax 的方式去局部修改 html 页面,都只是对内存中的 DOM 树进行修改;
- 浏览器环境中只有一个全局作用域,若 HTML 文档中链接有多个外部 JS 文件,这些 JS 文件会共用一个全局作用域;若不同的外部 JS 文件中定义了两个同名函数,将使用浏览器最后看到的那个函数(即最后加载的那个);
单线程模式
客户端 JS 是按单线程模式工作,意味着在**执行 **JS 脚本或事件处理程序时浏览器必须停止 HTML 文档解析、渲染和响应用户操作。
浏览器中 JS 的执行过程分为两个阶段:
- 同步阶段:载入 JS 文件并执行(包括 <scipt> 中的内容),通常按代码在文件中出现的顺序执行;
- 异步事件驱动阶段:所有同步脚本执行完成后(异步 async 脚本可能还未执行完成),进入异步事件驱动阶段,触发事件,调用事件处理程序。
- 此时文档解析完成,但其他内容(图像可能还未载入完成),当所有资源载入完成并且异步脚本也执行完成后,document.readyState 状态变为 complete ,浏览器触发 load 事件。
defer 和 async
Q:JS 的载入和执行时间?HTML解析是否被阻塞? JS 加载的顺序? JS 执行的顺序?
没有 defer 或 async 的普通JS脚本【下载】和【解析执行】时都会阻塞页面,也就是 <script> 后面的 HTML 元素暂时不会被解析为 DOM 树,即阻塞页面渲染;
async异步下载:async 属性表示异步执行引入的 JS,与 defer 的区别在于
- 如果 JS 没有下载完成,HTML 解析和 JS 下载会同时进行,下载完成后立即执行(不同于 defer);
- 如果已经加载好就会开始执行--无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。
需要注意的是,这种方式加载的 JS 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行;
defer 延迟执行:与 async 相同,加载 JS 和解析 HTML 会并行进行
- 当文档解析结束( DOM 树构建完成)并且 JS 都完成加载后再执行所有 defer 加载的 JS 文件(defer 属性的 JS 文件),然后触发 DOMContentLoaded 事件(标志着JS程序执行从同步阶段转换到异步事件驱动阶段)。
defer 与相比普通 script标签,有两点区别:
- 载入 JS 文件时不阻塞 HTML 的解析;
- 执行阶段被放到 HTML 标签解析完成之后。
在加载多个 JS 脚本的时候,async 是无顺序的加载,而 defer 是有顺序的加载。
async 的优先级高于 defer,同时应用到<script> 时忽略 defer。
DOMContentLoaded 和 load
- DOMContentLoaded:HTML 解析完成即 DOM 树构建完成时触发( defer 脚本执行完成),此时样式表、图片等资源可能尚未加载完成;
- load :页面上所有的 DOM、样式表、脚本、图片都已加载完成,async 脚本加载并执行完成时触发; onLoad事件处理程序只能有一个,即使注册了多个处理程序也只会执行其中一个;
document.addEventListener('DOMContentLoaded',function(){
console.log('3 seconds passed');
});
window.addEventListener('load', (event) => {
console.log('page is fully loaded');
});
总结
- 有 defer 和 async 时,下载脚本时可以继续解析 HTML 和渲染文档;
- defer 延迟脚本执行,在 HTML 解析(DOM 树构建完成)和脚本( JS )载入都完成时开始执行脚本(此时能访问完整的 DOM 树),defer 修饰的脚本会按他们在文档中出现的顺序加载和执行;
- async 下载时也不会阻塞文档解析,JS 载入完成后就开始执行,async 修饰的脚本可能会无序执行(看谁先载入),无法控制执行顺序,当页面的脚本之间彼此独立没有依赖时,async 是最理想的选择;
- 顺序:DOM树构建完成-->执行 defer 脚本-->触发 DOMContentLoaded 事件--->下载静态资源和执行异步脚本--->触发 load 事件
V8 引擎
参考
- 为什么建议 CSS 放在 <header> <scipt> 放在尾部?
- 为什么避免使用 CSS @import 导入 CSS 文件?
- 操作DOM具体的成本,说到底是造成浏览器回流reflow和重绘reflow,从而消耗GPU资源。
预加载的 <link>
尽快获取 JavaScript,而不会阻塞渲染
<head>
...
<!-- 预加载 JavaScript 文件 -->
<link rel="preload" href="important-js.js" as="script" />
<!-- 预加载 JavaScript 模块 -->
<link rel="modulepreload" href="important-module.js" />
...
</head>
浏览器工作原理与实践
插件 | 扩展程序
由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包。
页面
background、content-script、page、前台(popup各种页面)
browser action、page action
chrome.browserAction.setPopup()
- popup
- 背景页面
- Options 页面
content-scripts
Content script脚本是指能够在浏览器已经加载的页面内部运行的 javascript脚本。可以将content script看作是网页的一部分,而不是它所在的应用(扩展)的一部分。
Content script是在一个特殊环境中运行的,这个环境成为isolated world(隔离环境)。它们可以访问所注入页面的DOM,但是不能访问里面的任何javascript变量和函数
contentscript.js
================
var port = chrome.extension.connect();
document.getElementById('myCustomEventDiv').addEventListener('myCustomEvent', function() {
var eventData = document.getElementById('myCustomEventDiv').innerText;
port.postMessage({message: "myCustomEvent", values: eventData});
});
Content-script 只能访问以下api
chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
chrome.i18n
chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
chrome.storage
限制
- 不能使用除了chrome.extension之外的chrome.* 的接口
- 不能访问它所在扩展中定义的函数和变量
- 不能访问web页面或其它content script中定义的函数和变量
- 不能做cross-site XMLHttpRequests
Content scripts 可以使用messages机制与它所在的扩展通信,来间接使用chrome.*接口,或访问扩展数据。Content scripts还可以通过共享的DOM来与web页面通信。更多功能参见执行环境。
注意content-script 注入脚本 :解决 content-script 不能操作 DOM,在 content-script 中向 page 插入 script
标签,然后利用 page 和 content-script 共享 DOM 的原理使用自定义事件实现 page 和 content-script 通信。
页面间通信
content-script 发消息给 extension: runtime.sendMessage
extension 发消息给 content-script: tabs.sendMessage
可以使用chrome.extension
中的方法来获取应用(扩展)中的页面,例如getViews()和getBackgroundPage()。一旦一个页面得到了对应用(扩展)中其它页面的引用,它就可以调用被引用页面中的函数,并操作被引用页面的DOM树。
chrome.extension.getBackgroundPage();
// 在通知中调用扩展页面方法...
chrome.extension.getBackgroundPage().doThing();
// 从扩展页面调用通知的方法...
chrome.extension.getViews() 方法会返回属于你的扩展的每个活动页面的窗口对象列表
chrome.extension.getViews({type:"notification"}).forEach(function(win) {
win.doOtherThing();
});
- chrome.extension.sendRequest
contentscript.js
================
chrome.extension.sendRequest({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
// chrome.tabs.sendRequest
chrome.extension.onRequest.addListener(function (request, sender, sendResponse) {
console.log(sender.tab ? 'from a content script:' + sender.tab.url : 'from the extension');
if (request.greeting == 'hello') sendResponse({ farewell: 'goodbye' });
else sendResponse({}); // snub them.
});
- 长连接
建立一个长时间存在的通道从content script到扩展
使用chrome.extension.connect()或者chrome.tabs.connect()方法
- 获取url
var imgURL = **chrome.extension.getURL("images/myimage.png")**;
document.getElementById("someImage").src = imgURL;
- content 脚本和 background脚本通信
Content scripts should use runtime.sendMessage
to communicate with the background script.
注意
- 在 content-script 中不能直接操作 DOM,可以将操作 DOM 的代码编写到单独的 JS 文件中,然后使用
<script>
标签插入到 DOM中。 - 使用
<script>
标签插入的 JS 文件中的代码不能访问 chrome.* 的各种 API,只能在 content-script 中访问 chrome.* - 同一个页面中执行多次
chrome.runtime.onMessage.addListener
,只有第一次调用sendResponse
的有效,其他的被忽略。参考
参考
Chrome 扩展开发文档【干货】Chrome插件(扩展)开发全攻略chrome插件开发Could not establish connection. Receiving end does not exist
- 常用插件
- Window Resizer
- Allow CORS
- Web Vitals