瀏覽器的內核是指支持瀏覽器運行的最核心的程序,分為兩個部分的,一是渲染引擎,另一個是 JS 引擎。渲染引擎在不同的瀏覽器中也不是都相同的。目前市面上常見的瀏覽器內核可以分為這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。這里面大家最耳熟能詳的可能就是 Webkit 內核了,Webkit 內核是當下瀏覽器世界真正的霸主。
本文我們就以 Webkit 為例,對現代瀏覽器的渲染過程進行一個深度的剖析。
想閱讀更多優(yōu)質文章請猛戳GitHub 博客。
在介紹瀏覽器渲染過程之前,我們簡明扼要介紹下頁面的加載過程,有助于更好理解后續(xù)渲染過程。
要點如下:
例如在瀏覽器輸入https://juejin.im/timeline,然后經過 DNS 解析,juejin.im對應的 IP 是36.248.217.149(不同時間、地點對應的 IP 可能會不同)。然后瀏覽器向該 IP 發(fā)送 HTTP 請求。
服務端接收到 HTTP 請求,然后經過計算(向不同的用戶推送不同的內容),返回 HTTP 請求,返回的內容如下:
其實就是一堆 HMTL 格式的字符串,因為只有 HTML 格式瀏覽器才能正確解析,這是 W3C 標準的要求。接下來就是瀏覽器的渲染過程。
瀏覽器渲染過程大體分為如下三部分:
1)瀏覽器會解析三個東西:
一是 HTML/SVG/XHTML,HTML 字符串描述了一個頁面的結構,瀏覽器會把 HTML 結構字符串解析轉換 DOM 樹形結構。
二是 CSS,解析 CSS 會產生 CSS 規(guī)則樹,它和 DOM 結構比較像。
三是 Javascript 腳本,等到 Javascript 腳本文件加載后, 通過 DOM API 和 CSSOM API 來操作 DOM Tree 和 CSS Rule Tree。
2)解析完成后,瀏覽器引擎會通過 DOM Tree 和 CSS Rule Tree 來構造 Rendering Tree。
3)最后通過調用操作系統(tǒng) Native GUI 的 API 繪制。
接下來我們針對這其中所經歷的重要步驟詳細闡述
瀏覽器會遵守一套步驟將 HTML 文件轉換為 DOM 樹。宏觀上,可以分為幾個步驟:
瀏覽器從磁盤或網絡讀取 HTML 的原始字節(jié),并根據文件的指定編碼(例如 UTF-8)將它們轉換成字符串。
在網絡中傳輸的內容其實都是 0 和 1 這些字節(jié)數據。當瀏覽器接收到這些字節(jié)數據以后,它會將這些字節(jié)數據轉換為字符串,也就是我們寫的代碼。
將字符串轉換成 Token,例如:<html>、<body>等。Token 中會標識出當前 Token 是“開始標簽”或是“結束標簽”亦或是“文本”等信息。
這時候你一定會有疑問,節(jié)點與節(jié)點之間的關系如何維護?
事實上,這就是 Token 要標識“起始標簽”和“結束標簽”等標識的作用。例如“title”Token 的起始標簽和結束標簽之間的節(jié)點肯定是屬于“head”的子節(jié)點。
上圖給出了節(jié)點之間的關系,例如:“Hello”Token 位于“title”開始標簽與“title”結束標簽之間,表明“Hello”Token 是“title”Token 的子節(jié)點。同理“title”Token 是“head”Token 的子節(jié)點。
事實上,構建 DOM 的過程中,不是等所有 Token 都轉換完成后再去生成節(jié)點對象,而是一邊生成 Token 一邊消耗 Token 來生成節(jié)點對象。換句話說,每個 Token 被生成后,會立刻消耗這個 Token 創(chuàng)建出節(jié)點對象。注意:帶有結束標簽標識的 Token 不會創(chuàng)建節(jié)點對象。
接下來我們舉個例子,假設有段 HTML 文本:
復制代碼
<html> <head> <title>Web page parsing</title> </head> <body> <div> <h1>Web page parsing</h1> <p>This is an example Web page.</p> </div> </body> </html>
上面這段 HTML 會解析成這樣:
DOM 會捕獲頁面的內容,但瀏覽器還需要知道頁面如何展示,所以需要構建 CSSOM。
構建 CSSOM 的過程與構建 DOM 的過程非常相似,當瀏覽器接收到一段 CSS,瀏覽器首先要做的是識別出 Token,然后構建節(jié)點并生成 CSSOM。
在這一過程中,瀏覽器會確定下每一個節(jié)點的樣式到底是什么,并且這一過程其實是很消耗資源的。因為樣式你可以自行設置給某個節(jié)點,也可以通過繼承獲得。在這一過程中,瀏覽器得遞歸 CSSOM 樹,然后確定具體的元素到底是什么樣式。
注意:CSS 匹配 HTML 元素是一個相當復雜和有性能問題的事情。所以,DOM 樹要小,CSS 盡量用 id 和 class,千萬不要過渡層疊下去。
當我們生成 DOM 樹和 CSSOM 樹以后,就需要將這兩棵樹組合為渲染樹。
在這一過程中,不是簡單的將兩者合并就行了。渲染樹只會包括需要顯示的節(jié)點和這些節(jié)點的樣式信息,如果某個節(jié)點是 display: none 的,那么就不會在渲染樹中顯示。
我們或許有個疑惑:瀏覽器如果渲染過程中遇到 JS 文件怎么處理?
渲染過程中,如果遇到<script>就停止渲染,執(zhí)行 JS 代碼。因為瀏覽器有 GUI 渲染線程與 JS 引擎線程,為了防止渲染出現不可預期的結果,這兩個線程是互斥的關系。JavaScript 的加載、解析與執(zhí)行會阻塞 DOM 的構建,也就是說,在構建 DOM 時,HTML 解析器若遇到了 JavaScript,那么它會暫停構建 DOM,將控制權移交給 JavaScript 引擎,等 JavaScript 引擎運行完畢,瀏覽器再從中斷的地方恢復 DOM 構建。
也就是說,如果你想首屏渲染的越快,就越不應該在首屏就加載 JS 文件,這也是都建議將 script 標簽放在 body 標簽底部的原因。當然在當下,并不是說 script 標簽必須放在底部,因為你可以給 script 標簽添加 defer 或者 async 屬性(下文會介紹這兩者的區(qū)別)。
JS 文件不只是阻塞 DOM 的構建,它會導致 CSSOM 也阻塞 DOM 的構建。
原本 DOM 和 CSSOM 的構建是互不影響,井水不犯河水,但是一旦引入了 JavaScript,CSSOM 也開始阻塞 DOM 的構建,只有 CSSOM 構建完畢后,DOM 再恢復 DOM 構建。
這是什么情況?
這是因為 JavaScript 不只是可以改 DOM,它還可以更改樣式,也就是它可以更改 CSSOM。因為不完整的 CSSOM 是無法使用的,如果 JavaScript 想訪問 CSSOM 并更改它,那么在執(zhí)行 JavaScript 時,必須要能拿到完整的 CSSOM。所以就導致了一個現象,如果瀏覽器尚未完成 CSSOM 的下載和構建,而我們卻想在此時運行腳本,那么瀏覽器將延遲腳本執(zhí)行和 DOM 構建,直至其完成 CSSOM 的下載和構建。也就是說,在這種情況下,瀏覽器會先下載和構建 CSSOM,然后再執(zhí)行 JavaScript,最后在繼續(xù)構建 DOM。
當瀏覽器生成渲染樹以后,就會根據渲染樹來進行布局(也可以叫做回流)。這一階段瀏覽器要做的事情是要弄清楚各個節(jié)點在頁面中的確切位置和大小。通常這一行為也被稱為“自動重排”。
布局流程的輸出是一個“盒模型”,它會精確地捕獲每個元素在視口內的確切位置和尺寸,所有相對測量值都將轉換為屏幕上的絕對像素。
布局完成后,瀏覽器會立即發(fā)出“Paint Setup”和“Paint”事件,將渲染樹轉換成屏幕上的像素。
以上我們詳細介紹了瀏覽器工作流程中的重要步驟,接下來我們討論幾個相關的問題:
1.async 和 defer 的作用是什么?有什么區(qū)別?
接下來我們對比下 defer 和 async 屬性的區(qū)別:
其中藍色線代表 JavaScript 加載;紅色線代表 JavaScript 執(zhí)行;綠色線代表 HTML 解析。
1)情況 1<script src="script.js"></script>
沒有 defer 或 async,瀏覽器會立即加載并執(zhí)行指定的腳本,也就是說不等待后續(xù)載入的文檔元素,讀到就加載并執(zhí)行。
2)情況 2<script async src="script.js"></script> (異步下載)
async 屬性表示異步執(zhí)行引入的 JavaScript,與 defer 的區(qū)別在于,如果已經加載好,就會開始執(zhí)行——無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(fā)之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發(fā)之前或之后執(zhí)行,但一定在 load 觸發(fā)之前執(zhí)行。
3)情況 3 <script defer src="script.js"></script>(延遲執(zhí)行)
defer 屬性表示延遲執(zhí)行引入的 JavaScript,即這段 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關),會執(zhí)行所有由 defer-script 加載的 JavaScript 代碼,然后觸發(fā) DOMContentLoaded 事件。
defer 與相比普通 script,有兩點區(qū)別:載入 JavaScript 文件時不阻塞 HTML 的解析,執(zhí)行階段被放到 HTML 標簽解析完成之后。
在加載多個 JS 腳本的時候,async 是無順序的加載,而 defer 是有順序的加載。
2. 為什么操作 DOM 慢?
把 DOM 和 JavaScript 各自想象成一個島嶼,它們之間用收費橋梁連接。——《高性能 JavaScript》
JS 是很快的,在 JS 中修改 DOM 對象也是很快的。在 JS 的世界里,一切是簡單的、迅速的。但 DOM 操作并非 JS 一個人的獨舞,而是兩個模塊之間的協(xié)作。
因為 DOM 是屬于渲染引擎中的東西,而 JS 又是 JS 引擎中的東西。當我們用 JS 去操作 DOM 時,本質上是 JS 引擎和渲染引擎之間進行了“跨界交流”。這個“跨界交流”的實現并不簡單,它依賴了橋接接口作為“橋梁”(如下圖)。
過“橋”要收費——這個開銷本身就是不可忽略的。我們每操作一次 DOM(不管是為了修改還是僅僅為了訪問其值),都要過一次“橋”。過“橋”的次數一多,就會產生比較明顯的性能問題。因此“減少 DOM 操作”的建議,并非空穴來風。
3. 你真的了解回流和重繪嗎?
渲染的流程基本上是這樣(如下圖黃色的四個步驟):
1. 計算 CSS 樣式
2. 構建 Render Tree
3.Layout – 定位坐標和大小
4. 正式開畫
注意:上圖流程中有很多連接線,這表示了 Javascript 動態(tài)修改了 DOM 屬性或是 CSS 屬性會導致重新 Layout,但有些改變不會重新 Layout,就是上圖中那些指到天上的箭頭,比如修改后的 CSS rule 沒有被匹配到元素。
這里重要要說兩個概念,一個是 Reflow,另一個是 Repaint
重繪:當我們對 DOM 的修改導致了樣式的變化、卻并未影響其幾何屬性(比如修改了顏色或背景色)時,瀏覽器不需重新計算元素的幾何屬性、直接為該元素繪制新的樣式(跳過了上圖所示的回流環(huán)節(jié))。
回流:當我們對 DOM 的修改引發(fā)了 DOM 幾何尺寸的變化(比如修改元素的寬、高或隱藏元素等)時,瀏覽器需要重新計算元素的幾何屬性(其他元素的幾何屬性和位置也會因此受到影響),然后再將計算的結果繪制出來,這個過程就是回流(也叫重排)。
我們知道,當網頁生成的時候,至少會渲染一次。在用戶訪問的過程中,還會不斷重新渲染。重新渲染會重復回流 + 重繪或者只有重繪。
回流必定會發(fā)生重繪,重繪不一定會引發(fā)回流。重繪和回流會在我們設置節(jié)點樣式時頻繁出現,同時也會很大程度上影響性能?;亓魉璧某杀颈戎乩L高的多,改變父節(jié)點里的子節(jié)點很可能會導致父節(jié)點的一系列回流。
1)常見引起回流屬性和方法
任何會改變元素幾何信息 (元素的位置和尺寸大小) 的操作,都會觸發(fā)回流,
2)常見引起重繪屬性和方法
3)如何減少回流、重繪
復制代碼
for(let i = 0; i < 1000; i++) { // 獲取 offsetTop 會導致回流,因為需要去獲取正確的值 console.log(document.querySelector('.test').style.offsetTop) }
基于上面介紹的瀏覽器渲染原理,DOM 和 CSSOM 結構構建順序,初始化可以對頁面渲染做些優(yōu)化,提升頁面性能。
綜上所述,我們得出這樣的結論:
參考文章
更多內容,請關注前端之巔。
著css3實現各種炫酷動畫效果越來越流行。今天給大家推薦一些css3和SVG實現loading加載動畫效果。
先上一波令人愉悅的動畫效果。
怎么樣,是不是感覺很nice,那就繼續(xù)往下看吧。這里為大家整理了一些不錯的效果。
簡單酷炸的css3效果,可一鍵復制css樣式。
# 演示地址
https://cssfx.lovejade.cn/
# github地址
https://github.com/TheHumanComedy/cssfx
一個超棒的svg實現loading動畫效果。
# 演示地址
http://samherbert.net/svg-loaders/
# github地址
https://github.com/SamHerbert/SVG-Loaders
非常棒的一款CSS3加載動畫集合。star高達17.4K+。
# 演示地址
https://tobiasahlin.com/spinkit/
# github地址
https://github.com/tobiasahlin/SpinKit
一款有趣的單元素CSS3加載器。
# 演示地址
https://projects.lukehaas.me/css-loaders/
# github地址
https://github.com/lukehaas/css-loaders
基于canvas實現的loading效果。
https://github.com/padolsey/sonic.js
一組很酷的css3加載動畫效果。
# 演示地址
https://codepen.io/Beaugust/pen/DByiE
有一組優(yōu)秀的CSS3動畫效果。
# 演示地址
https://codepen.io/viduthalai1947/pen/JkhDK
一組SVG實現加載動畫效果。
# 演示地址
https://codepen.io/aurer/pen/jEGbA
好了,就介紹到這里。希望對大家有所幫助!
者開源了一個Web思維導圖,在做導出為圖片的功能時走了挺多彎路,所以通過本文來記錄一下。
思維導圖的節(jié)點和連線都是通過 svg渲染的,作為一個純 js 庫,我們不考慮通過后端來實現,所以只能思考如何通過純前端的方式來實現將svg或html轉換為圖片。
我們都知道 img 標簽可以顯示 svg,然后 canvas 又可以渲染 img,那么是不是只要將svg渲染到img標簽里,再通過canvas導出為圖片就可以呢,答案是肯定的。
const svgToPng = async (svgStr) => {
// 轉換成blob數據
let blob = new Blob([svgStr], {
type: 'image/svg+xml'
})
// 轉換成data:url數據
let svgUrl = await blobToUrl(blob)
// 繪制到canvas上
let imgData = await drawToCanvas(svgUrl)
// 下載
downloadFile(imgData, '圖片.png')
}
svgStr是要導出的svg字符串,比如:
然后通過Blob構造函數創(chuàng)建一個類型為image/svg+xml的blob數據,接下來將blob數據轉換成data:URL:
const blobToUrl = (blob) => {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = evt => {
resolve(evt.target.result)
}
reader.onerror = err => {
reject(err)
}
reader.readAsDataURL(blob)
})
}
其實就是base64格式的字符串。
接下來就可以通過img來加載,并渲染到canvas里進行導出:
const drawToCanvas = (svgUrl) => {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域圖片需要添加這個屬性,否則畫布被污染了無法導出圖片
img.setAttribute('crossOrigin', 'anonymous')
img.onload = async () => {
try {
let canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
let ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, img.width, img.height)
resolve(canvas.toDataURL())
} catch (error) {
reject(error)
}
}
img.onerror = e => {
reject(e)
}
img.src = svgUrl
})
}
canvas.toDataURL()方法返回的也是一個base64格式的data:URL字符串:
最后就可以通過a標簽來下載:
const downloadFile = (file, fileName) => {
let a = document.createElement('a')
a.href = file
a.download = fileName
a.click()
}
實現很簡單,效果也不錯,不過這樣就沒問題了嗎,接下來我們插入兩張圖片試試。
第一張圖片是使用base64的data:URL方式插入的,第二張圖片是使用普通url插入的:
導出結果如下:
可以看到,第一張圖片沒有問題,第二張圖片裂開了,可能你覺得同源策略的問題,但實際上換成同源的圖片,同樣也是裂開的,解決方法很簡單,遍歷svg節(jié)點樹,將圖片都轉換成data:URL的形式即可:
// 操作svg使用了@svgdotjs/svg.js庫
const transfromImg = (svgNode) => {
let imageList = svgNode.find('image')
let task = imageList.map(async item => {
// 獲取圖片url
let imgUlr = item.attr('href') || item.attr('xlink:href')
// 已經是data:URL形式不用轉換
if (/^data:/.test(imgUlr)) {
return
}
// 轉換并替換圖片url
let imgData = await drawToCanvas(imgUlr)
item.attr('href', imgData)
})
await Promise.all(task)
return svgNode.svg()// 返回svg html字符串
}
這里使用了前面的drawToCanvas方法來將圖片轉換成data:URL,這樣導出就正常了:
到這里,將純 svg 轉換為圖片就基本沒啥問題了。
svg提供了一個foreignObject標簽,可以插入html節(jié)點,實際上,筆者就是使用它來實現節(jié)點的富文本編輯效果的:
接下來使用前面的方式來導出,結果如下:
明明顯示沒有問題,導出時foreignObject內容卻發(fā)生了偏移,這是為啥呢,其實是因為默認樣式的問題,頁面全局清除了margin和padding,以及將box-sizing設置成了border-box:
那么當svg存在于文檔樹中時是沒有問題的,但是導出時使用的是svg字符串,是脫離于文檔的,所以沒有這個樣式覆蓋,那么顯示自然會出現問題,知道了原因,解決方法有兩種,一是遍歷所有嵌入的html節(jié)點,手動添加內聯樣式,注意一定要給所有的html節(jié)點都添加,只給svg、foreignObject或最外層的html節(jié)點添加都是不行的;第二種是直接在foreignObject標簽里添加一個style標簽,通過style標簽來加上樣式,并且只要給其中一個foreignObject標簽添加就可以了,兩種方式看你喜歡哪種,筆者使用的是第二種:
const transformForeignObject = (svgNode) => {
let foreignObjectList = svgNode.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>`))
}
return svgNode.svg()
}
導出結果如下:
可以看到,一切正常。
關于兼容性的問題,筆者測試了最新的chrome、firefox、opera、safari、360急速瀏覽器,運行都是正常的。
前面介紹的是筆者目前采用的方案,看著實現其實非常簡單,但是過程漫長且坎坷,接下來,開始我的表演。
對于svg的操作筆者使用的是svg.js庫,創(chuàng)建富文本節(jié)點的核心代碼大致如下:
import { SVG, ForeignObject } from '@svgdotjs/svg.js'
let html = `<div>節(jié)點文本</div>`
let foreignObject = new ForeignObject()
foreignObject.add(SVG(html))
g.add(foreignObject)
SVG方法是用來將一段html字符串轉換為dom節(jié)點的。
在chrome瀏覽器和opera瀏覽器上渲染非常正常,但是在firefox瀏覽器上foreignObject標簽的內容完全渲染不出來:
檢查元素也看不出有任何問題,并且神奇的是只要在控制臺元素里編輯一下嵌入的html內容,它就可以顯示了,百度搜索了一圈,也沒找到解決方法,然后因為firefox瀏覽器占有率并不高,于是這個問題就擱淺了。
chrome瀏覽器雖然渲染是正常的:
但是使用前面的方式導出時foreignObject標簽內容卻是跟在firefox瀏覽器里顯示一樣是空的:
firefox能忍這個不能忍,于是嘗試使用一些將html轉換為圖片的庫。
使用html2canvas:
import html2canvas from 'html2canvas'
const useHtml2canvas = async (svgNode) => {
let el = document.createElement('div')
el.style.position = 'absolute'
el.style.left = '-9999999px'
el.appendChild(svgNode)
document.body.appendChild(el)// html2canvas轉換需要被轉換的節(jié)點在文檔中
let canvas = await html2canvas(el, {
backgroundColor: null
})
mdocument.body.removeChild(el)
return canvas.toDataURL()
}
html2canvas可以成功導出,但是存在一個問題,就是foreignObject標簽里的文本樣式會丟失:
這應該是html2canvas的一個bug,不過看它這issues數量和提交記錄:
指望html2canvas改是不現實的,于是又嘗試使用dom-to-image:
import domtoimage from 'dom-to-image'
const dataUrl = domtoimage.toPng(el)
發(fā)現dom-to-image更不行,導出完全是空白的:
并且它上一次更新時間已經是五六年前,所以沒辦法,只能回頭使用html2canvas。
后來有人建議使用dom-to-image-more,粗略看了一下,它是在dom-to-image庫的基礎上修改的,嘗試了一下,發(fā)現確實可以,于是就改為使用這個庫,然后又有人反饋在一些瀏覽器上導出節(jié)點內容是空的,包括firefox、360,甚至chrome之前的版本都不行,筆者只能感嘆,太難了,然后又有人建議使用上一個大版本,可以解決在firefox上的導出問題,但是筆者試了一下,在其他一些瀏覽器上依舊存在問題,于是又在考慮要不要換回html2canvas,雖然它存在一定問題,但至少不是完全空的。
用的人多了,這個問題又有人提了出來,于是筆者又嘗試看看能不能解決,之前一直認為是firefox瀏覽器的問題,畢竟在chrome和opera上都是正常的,這一次就想會不會是svgjs庫的問題,于是就去搜它的issue,沒想到,還真的搜出來了issue,大意就是因為通過SVG方法轉換的dom節(jié)點是在svg的命名空間下,也就是使用document.createElementNS方法創(chuàng)建的,導致部分瀏覽器渲染不出來,歸根結底,這還是不同瀏覽器對于規(guī)范的不同實現導致的:
你說chrome很強吧,確實,但是無形中它阻止了問題的暴露。
知道了原因,那么修改也很簡單了,只要將SVG方法第二個參數設為true即可,或者自己來創(chuàng)建節(jié)點也可以:
foreignObject.add(document.createElemnt('div'))
果然,在firefox瀏覽器上正常渲染了。
解決了在firefox瀏覽器上foreignObject標簽為空的問題后,自然會懷疑之前使用img結合canvas導出圖片時foreignObject標簽為空會不會也是因為這個問題,同時了解了一下dom-to-image庫的實現原理,發(fā)現它也是通過將dom節(jié)點添加到svg的foreignObject標簽中實現將html轉換成圖片的,那么就很搞笑了,我本身要轉換的內容就是一個嵌入了foreignObject標簽的svg,使用dom-to-image轉換,它會再次把傳給它的svg添加到一個foreignObject標簽中,這不是套娃嗎,既然dom-to-image-more能通過foreignObject標簽成功導出,那么不用它必然也可以,到這里基本確信之前不行就是因為命名空間的問題。
果然,在去掉了dom-to-image-more庫后,重新使用之前的方式成功導出了,并且在firefox、chrome、opera、360等瀏覽器中都不存在問題,兼容性反而比dom-to-image-more庫好。
雖然筆者的實現很簡單,但是dom-to-image-more這個庫實際上有一千多行代碼,那么它到底多做了些什么呢,點個關注,我們下一篇文章再見。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。