浏览器的主要组成
浏览器的主要组成有:
浏览器核心
网络模块
渲染引擎
JS 解释器
其他
浏览器核心调用网络模块发送 HTTP 请求,渲染引擎根据 HTTP 的响应信息渲染页面
如果渲染引擎遇到了 script,要先让网络模块下载 js,下载完成后让 js 解释器执行 js 代码
HTML 的解析和阻塞
1.下载 HTML
2.解析 HTML
这里不同浏览器的实现不同,可能一边下载 HTML 一边就开始解析了,也可能是完全下载完再解析
3.解析 HTML 的过程是构建一个 DOM 树,不是直接渲染的
中途如果看到 css 就让网络模块去异步地下载 css 文件,然后继续解析下一行 HTML
解析 css 是构建一个 css 树,
中途如果看到的是 js 也就是 script 的标签,就会去让网络模块下载 js 文件资源然后执行,
但是和 css 不同,在遇到 js 后解析 HTML 的过程会停止,直到 js 执行完才会继续解析后面的 HTML
即:js 的下载和执行会阻塞 HTML 的解析
执行 js 的过程会阻塞 HTML 的解析:执行 js 可能会修改 HTML 的 DOM 树(比如新加一个 div),希望可以让 js 先修改完了再解析 DOM 树
下载 js 的过程会阻塞 HTML 的解析:解析是逐行的,只有解析到 script 标签才会下载,浏览器不做额外优化就会阻塞 HTML 的解析
async vs defer
async 和 defer 可以作为属性加在 script 标签上
首先在执行 js 的时候不能解析 HTML 依然不变
defer 可以让下载 js 时可以继续解析 HTML,当 js 下载并执行完毕后才会触发 DOM ready
async 也可以让下载 js 时可以继续解析 HTML,当 HTML 解析完毕后触发 DOM ready
defer与async的区别:
defer:多个带有defer属性的脚本会按照它们在文档中的顺序执行。
async:多个带有async属性的脚本的执行顺序不确定,取决于下载完成的时间。
另外还有
|
|
type=“module” 和 defer 几乎等价,下载 js 不阻塞 HTML 解析,执行完 js 后才出发 DOM ready
实际工作中 defer 更常见
下载和解析 css 会阻塞 js 的执行
因为 js 需要读取 css 的结果,比如 js 去获取一个元素的宽高,而这个宽高是由 css 解析结果决定的
关于页面的渲染
在得到 DOM 树和 CSS 树(CSSOM)后,会合成为渲染树(这里 DOM 树有完整的页面结构,CSS 树不一定有完整的页面结构,所以需要在真正渲染前合并两棵树为渲染做准备),渲染树(Render tree)有每一个 DOM 节点和对于的 CSS 内容
Layout 布局,元素的大小尺寸,计算高度宽度,计算当前屏幕区域展示的内容
Paint 绘制每一个元素的着色阴影
Composite 对多层次的内容合并成一层的内容,对处于同一个位置的元素根据层级关系渲染展示
最后输出到屏幕上
当用 js 改变 DOM 元素的位置大小的时候会
触发 reflow 重新计算布局
然后再 repaint 重新绘制
重新合成不一定会触发
CSS Triggers 有列举不同属性改变触发不同渲染阶段的结论
web 新能指标
从用户跳转到当前网页后:
- 有内容出现
- DOM ready(DOM content loaded) 事件发生 HTML 的内容全部解析完,js 基本执行完
- 页面可交互时
- onLoad 事件发生
- 动态资源加载完
DNS 优化-预解析
对资源的 URI 做 DNS 查询的时候,会逐行解析,然后(DNS 查询 + 下载资源) + (DNS 查询 + 下载资源) * N…,通过 DNS 预解析可以提前同时查多个 DNS,就节约了后面单个 DNS 查询的时间
前端可以在 index.html 的<head>
中写
|
|
后端可以在 index.html 返回的响应头里写
Link: <https://xx.com/>; rel=dns-prefetch
TCP 优化连接复用
在 HTTP 1.1 之前很多请求都是默认不持久化的,意思就是每次发一个 HTTP 请求都需要重新建立一次 TCP 连接,且这次 TCP 连接在 HTTP 请求结束后也会关闭
通过持续建立 TCP 连接,可以节约重复关闭和建立 TCP 连接的时间
通过在 HTTP 的请求头和响应头中添加头部字段 Connection: keep-alive 就可以使得 TCP 连接持久化-实现连接复用
细节:在请求和响应之间间隔多少才回认为在一次持久连接中呢?猜测有一个地方设置间隔时间
KeepAlive: timeout=60, max=120
可以是浏览器发送的,也可以是服务器发送的,需要双方协商,协商的方式就是回应的时候带上自己的 KeepAlive
TCP 优化并行连接
HTTP 1.1 已经自带了并行连接,但是并行连接有最大并行数量(如果是以域名为标记的,比如对 a.com 的访问最多并行数是 12,那如果 cdn 换多个子域名就可以突破并行限制了;浏览器本身也有一个最大请求并行上限)
把 js 和 css 各自拆成 2-4 个可以利用并行同时下载,提高下载速度
HTTP/1 管道化 pipelining
设想是在一次 HTTP/1 请求中并行发多个请求,各自的请求返回各自的结果,但是由于 HTTP/1 是无状态的加上网络传输的不确定性,无法知道哪个结果是哪个请求的
所以在 HTTP/1 中管道化是一个有 bug 的设计
HTTP/2 多路复用和服务器推送
在 HTTP/2 中提出了帧和流的概念,就能做到 HTTP/1 管道化想做到的事情,即:在一次连接中发送多个请求,且接收端可以请求时的 id 来标记,就知道哪个结果是哪个请求发送的了,而且由于帧概念的提出资源的单位更小了也响应加上了自动整合帧的功能。
更多信息可以看这个博客:浅析 HTTP/2 的多路复用
HTTP/2 还可以做服务器推送,比如在浏览器第一次请求时服务器就可以预先响应推送多个 css 和 js 给浏览器,预判浏览器接下来需要请求的内容,提前一次性推送给浏览器。之后在解析 HTML 的过程中再遇到这几个 css 和 js 时,浏览器就会直接返回响应内容。
要达到服务器推送需要预先人为设置,可以在服务器中设置 HTTP2_push 的文件,也可以在服务器的响应头里添加设置。但是目前前端的 js 和 css 名字在打包后是带 hash 的经常会更新改动,导致人为预先设置会很麻烦,所以在实际情况里很少使用服务器推送。
HTTP/1.1 中的 web 性能优化:
合并、内联、压缩、精简
CDN、缓存
合并资源
css 雪碧图(把多个图片合并到一个大图中,用 css 绝对定位来取区块内容)webpack 有插件方便实现 webpack-spritesmith
Icon Font (把多个图标合并变成一个字体文件,用 css 的 class 来选择使用)
SVG Symbols (把多个图标合并变成一个 icon.svg 文件,用,相比 Icon Font 支持渐变,.svg 文件编辑更方便)
资源内联
把小文件的内容直接内联写到 html 中
图片用 Data URLs,webpack 也有相应的插件 url-loader,加设置 limit:xxx,小于 xxx 的图片会自动转为 Data URLs
css 文件 用,webpack 插件有: inlineSourcePlugin(不再维护,依赖 html-webpack-plugin)、 inlineChunkHtmlPlugin
js 文件 用
资源压缩
常用的压缩方式是 gzip,一般在服务器端设置开始压缩
在请求头中有字段 Accept-Encoding: gzip, deflate 表示用户代理支持的内容编码方式和优先级
代码精简
HTML 删空格、闭合
CSS 删除未使用
JS 改名,tree Shaking(分析 import 的内容哪些用了哪些没用,但是 require 不能很好地分析,require(变量))
SVG 删除无用标签,属性
图片 减少内容(无损、有损压缩)
CDN 内容分发网络
在物理的角度上缩短请求的距离,在距离请求近的地方做缓存服务器
DNS 解析有负载均衡同一个域名可能会有多个 ip 的选择
CDN 一般不和主站在同级域名,防止在请求中带有主站的 cookie,来减少请求信息
可以用命令行上传文件到 cdn,再用相关命令修改文件的路径为 cdn 路径
npm build publicUrl = ‘https://1.cdnx.com’
CDN 好处:
- cookie free
- 并行请求/多路复用
- 下载速度快,只用在静态内容
CDN 害处:
- 要钱
- 部署变复杂
- 可控性变差
- 跨域 可以用 CORS 解决,关于 CORS
跨域的脚本 Error 拿不到具体的错误信息和堆栈信息-解决办法
缓存 & 内容协商
也称为(强缓存和弱缓存)
Cache-Control:
public/private,
max-age: 3600,
must-revalidate
public 公开的信息,中间的节点都可以缓存
private 私人的信息 只有用户端和服务器端可以缓存
max-age 缓存时间,最久有效期单位秒 3600 代表一个小时
must-revalidate 如果缓存过期,必须重新校验
协商请求过程:
- 用户向 CDN 服务器请求 main-1.js
- 服务器返回响应内容,响应头中有 Cache-Control 1 小时,ETag xxx
- 用户的浏览器缓存 main-1.js
- 一个小时后 main-1.js 过期了,如果还需要获取 main-1.js,就需要再次请求服务器
这次的请求就是协商请求会带上 main-1.js 的 ETag,请求头有 If-None-Match: xxx(ETag 的值) - 服务器把接收的 ETag 和实际文件的 ETag 做对比,
如果没变就返回 304 且没有响应体也就是没有变可以继续沿用过期的内容,
如果变了就返回 200 和新的 main-1.js 的内容,那就需要删除(没有 cache-control 或者 max-age=0)或更新(有 cache-control)覆盖缓存中 main-1.js 的内容
ETag 是文件的哈希散列值
服务器主动禁用缓存
如果响应中不加 Cache-Control,浏览器也会默认缓存的
禁止缓存可以在响应头里添加:
Cache-Control: max-age=0, must-revalidate 不缓存,有协商
Cache-Control: no-cache 不缓存,有协商
Cache-Control: no-store 不协商,不协商
浏览器主动禁用缓存
方法 1 只要 url 不一样就会跳过缓存
比如:get 请求 xxx.com/home 如果改为 xxx.com/home?a=1 加一个随机数
方法 2 设置请求头
Cache-Control: no-cache no-store max-age=0
代码的位置
css 放在头部的原因:
1 css 不阻塞 html 的解析,而且用户最先关注的是页面的显示,所以 css 应该尽早下载
2 此外也可以防止 css 被 js 阻塞
内联的 js 推荐放在头部的 css 前的原因:
1 内联的 js 没有网络请求执行起来很快,不用等 css 下载解析
js 放在 body 最后的原因:
1 可直接访问页面的 DOM,无需监听 DOM ready
2 避免阻塞 html 的解析过程
代码拆分
js 和 css 都做区分,如果都放在一个文件里,每次更新用户都需要重新下载,
拆分后不变的部分用户可以用缓存,变的部分再重新请求
js 动态导入
使用库时的动态导入
|
|
路由加载组件的动态导入
Vue
|
|
React
|
|
图片懒加载
1. <img src="product1.jpg"/>
2. <img src="placeholder.png"
data-src="prodcut1.jpg/>
3. 监听windows on scroll
findImgs().each img 每有一个img就生成一个新的new image对象
new Image()
.src = img.data.src 去请求真正的图片
.onload img.src = img.dataset.src //请求完毕后替换placeholder的图片
预加载 提前出发懒加载的时间,优化用户体验
css 代码优化
删除无用 css
减少重排比如 left 动画改成 transform
link 可以并行加载
@import 只能串行加载
启用 GPU 硬件加速:给元素加上 transform: translate3d(0,0,0)
缩写 #FFFFFF -> #FFF ; 0.1 -> .1 ; 0px -> 0
砍需求