CSS
浏览器、网络
浏览器
从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示
为什么两指放大,渲染速度如此之快
网络
HTTP、TCP/IP
三次握手
你好,我是x;你好x,我是y;你好y。
• 发送端首先发送一个带SYN标志的数据包给对方。接收端收到后,回传一个带有SYN/ACK标志的数据包以示传达确认信息。 最后,发送端再回传一个带ACK标志的数据包,代表“握手”结束。 若在握手过程中某个阶段莫名中断,TCP协议会再次以相同的顺序发送相同的数据包
四次挥手
第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
MSL是指Max Segment Lifetime
💡 TIME_WAIT状态存在的原因主要有两点:1)为实现TCP这种全双工(full-duplex)连接的可靠释放,假设主动发起方(图中为左侧)发送的ACK(4次交互的最后一个包)在网络中丢失,那么由于TCP的重传机制,被动关闭方需要重发其FIN,如果主动方没有TIME_WAIT状态,等这条重发的FIN进来,会被对方认为有错误发生;2)为使旧的数据包在网络因过期而消失SSL/TLS
HTTP 1.0 、1.1、2.0 、 3.0
http1.0 exprires last-modified 连接无法复用
http1.1 etag cache-control 支持长连接(connection) 支持文件断点续传
http2.0 多路复用 首部压缩 server push 传输速度更快了
HTTP 1.0
- 无状态,无连接
- 短连接:每次发送请求都要重新建立tcp请求,即三次握手,非常浪费性能
- 无host头域,也就是http请求头里的host,
- 不允许断点续传,而且不能只传输对象的一部分,要求传输整个对象
HTTP 1.1
- 长连接,流水线,使用connection:keep-alive使用长连接
- 请求管道化
- 增加缓存处理(新的字段如cache-control)
- 增加Host字段,支持断点传输等
- 由于长连接会给服务器造成压力
HTTP 2.0
- 二进制分帧
- 头部压缩,双方各自维护一个header的索引表,使得不需要直接发送值,通过发送key缩减头部大小
- 多路复用(或连接共享),使用多个stream,每个stream又分帧传输,使得一个tcp连接能够处理多个http请求
- 服务器推送(Sever push)
- 请求优先级:使用 HTTP 2.0,我们可以在一批请求中设置数字优先级。因此,我们可以明确我们期望响应的顺序,例如在 JS 文件之前获取网页 CSS
- 自动压缩:在以前的HTTP版本(1.1)中,我们必须明确要求压缩请求和响应。然而,HTTP 2.0 会自动执行 GZip 压缩
- 连接重置:一种功能,允许出于某种原因关闭服务器和客户端之间的连接,从而立即打开一个新连接
从图中可以看出,HTTP/2 添加了一个二进制分帧层,那我们就结合图来分析下 HTTP/2 的请求和接收过程。
- 首先,浏览器准备好请求数据,包括了请求行、请求头等信息,如果是 POST 方法,那么还要有请求体。
- 这些数据经过二进制分帧层处理之后,会被转换为一个个带有请求 ID 编号的帧,通过协议栈将这些帧发送给服务器。
- 服务器接收到所有帧之后,会将所有相同 ID 的帧合并为一条完整的请求信息。
- 然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。
- 同样,二进制分帧层会将这些响应数据转换为一个个带有请求 ID 编号的帧,经过协议栈发送给浏览器。
- 浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求
从上面的流程可以看出,通过引入二进制分帧层,就实现了 HTTP 的多路复用技术。
HTTP 3.0
- 基于google的QUIC协议,而quic协议是使用udp实现的
- 减少了tcp三次握手时间,以及tls握手时间
- 解决了http 2.0中前一个stream丢包导致后一个stream被阻塞的问题
- 优化了重传策略,重传包和原包的编号不同,降低后续重传计算的消耗
- 连接迁移,不再用tcp四元组确定一个连接,而是用一个64位随机数来确定这个连接
- 更合适的流量控制
http3.0 QUIC(快速 UDP 互联网连接),QUIC 是一种具有本机多路复用和内置加密功能的传输层协议
chfs cute http file server
缓存
强缓存
expires、max-age
Cache-Control
-
public表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有max-age指令或Expires消息头;2. 该响应对应的请求方法是 POST 。) -
private表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。 -
no-cache在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证 (协商缓存验证)。 -
no-store缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。
协商缓存
| 浏览器 | 服务器 |
|---|---|
| if-modified-since | Last-Modified |
| if-none-match | Etag |
etag优先级高于last-modified
安全
XSS
XSS 全称是 Cross Site Scripting,为了与“CSS”区分开来,故简称 XSS,翻译过来就是“跨站脚本”。XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
- 存储型 XSS 攻击 利用漏洞存储到数据库,其他用户访问了带有恶意 JavaScript 脚本的页面,就会泄露信息。
- 反射型 XSS 攻击 在一个反射型 XSS 攻击过程中,恶意 JavaScript 脚本属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript 脚本返回给用户。当恶意 JavaScript 脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作。 另外需要注意的是,Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方。
- 基于 DOM 的 XSS 攻击 基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据
如何阻止 XSS 攻击
我们知道存储型 XSS 攻击和反射型 XSS 攻击都是需要经过 Web 服务器来处理的,因此可以认为这两种类型的漏洞是服务端的安全漏洞。而基于 DOM 的 XSS 攻击全部都是在浏览器端完成的,因此基于 DOM 的 XSS 攻击是属于前端的安全漏洞。
但无论是何种类型的 XSS 攻击,它们都有一个共同点,那就是首先往浏览器中注入恶意脚本,然后再通过恶意脚本将用户信息发送至黑客部署的恶意服务器上。
所以要阻止 XSS 攻击,我们可以通过阻止恶意 JavaScript 脚本的注入和恶意消息的发送来实现。
接下来我们就来看看一些常用的阻止 XSS 攻击的策略
#1. 服务器对输入脚本进行过滤或转码
不管是反射型还是存储型 XSS 攻击,我们都可以在服务器端将一些关键的字符进行转码,比如最典型的:
code:<script>alert('你被 xss 攻击了')</script>
这段代码过滤后,只留下了:code
这样,当用户再次请求该页面时,由于<script>标签的内容都被过滤了,所以这段脚本在客户端是不可能被执行的。
除了过滤之外,服务器还可以对这些内容进行转码,还是上面那段代码,经过转码之后,效果如下所示:
code:<script>alert(' 你被 xss 攻击了 ')</script>
经过转码之后的内容,如
<script>标签被转换为<script>,因此即使这段脚本返回给页面,页面也不会执行这段脚本
#2. 充分利用 CSP
虽然在服务器端执行过滤或者转码可以阻止 XSS 攻击的发生,但完全依靠服务器端依然是不够的,我们还需要把 CSP 等策略充分地利用起来,以降低 XSS 攻击带来的风险和后果。
实施严格的 CSP 可以有效地防范 XSS 攻击,具体来讲 CSP 有如下几个功能:
- 限制加载其他域下的资源文件,这样即使黑客插入了一个 JavaScript 文件,这个 JavaScript 文件也是无法被加载的;
- 禁止向第三方域提交数据,这样用户数据也不会外泄;
- 禁止执行内联脚本和未授权的脚本;
- 还提供了上报机制,这样可以帮助我们尽快发现有哪些 XSS 攻击,以便尽快修复问题。
因此,利用好 CSP 能够有效降低 XSS 攻击的概率。
#3. 使用 HttpOnly 属性
由于很多 XSS 攻击都是来盗用 Cookie 的,因此还可以通过使用 HttpOnly 属性来保护我们 Cookie 的安全。
通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的,下面是打开 Google 时,HTTP 响应头中的一段
set-cookie: NID=189=M8q2FtWbsR8RlcldPVt7qkrqR38LmFY9jUxkKo3-4Bi6Qu_ocNOat7nkYZUTzolHjFnwBw0izgsATSI7TZyiiiaV94qGh-BzEYsNVa7TZmjAYTxYTOM9L_-0CN9ipL6cXi8l6-z41asXtm2uEwcOC5oh9djkffOMhWqQrlnCtOI; expires=Sat, 18-Apr-2020 06:52:22 GMT; path=/; domain=.google.com; HttpOnly
我们可以看到,set-cookie 属性值最后使用了 HttpOnly 来标记该 Cookie。顾名思义,使用 HttpOnly 标记的 Cookie 只能使用在 HTTP 请求过程中,所以无法通过 JavaScript 来读取这段 Cookie。我们还可以通过 Chrome 开发者工具来查看哪些 Cookie 被标记了 HttpOnly,如下图:
从图中可以看出,NID 这个 Cookie 的 HttpOlny 属性是被勾选上的,所以 NID 的内容是无法通过 document.cookie 是来读取的。
由于 JavaScript 无法读取设置了 HttpOnly 的 Cookie 数据,所以即使页面被注入了恶意 JavaScript 脚本,也是无法获取到设置了 HttpOnly 的数据。因此一些比较重要的数据我们建议设置 HttpOnly 标志。
CSRF
CSRF 英文全称是 Cross-site request forgery,所以又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。 和 XSS 不同的是,CSRF 攻击不需要将恶意代码注入用户的页面,仅仅是利用服务器的漏洞和用户的登录状态来实施攻击
1. 充分利用好 Cookie 的 SameSite 属性
SameSite 选项通常有 Strict、Lax 和 None 三个值。
- Strict 最为严格。如果 SameSite 的值是 Strict,那么浏览器会完全禁止第三方 Cookie。简言之,如果你从极客时间的页面中访问 InfoQ 的资源,而 InfoQ 的某些 Cookie 设置了 SameSite = Strict 的话,那么这些 Cookie 是不会被发送到 InfoQ 的服务器上的。只有你从 InfoQ 的站点去请求 InfoQ 的资源时,才会带上这些 Cookie。
- Lax 相对宽松一点。在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交 Get 方式的表单这两种方式都会携带 Cookie。但如果在第三方站点中使用 Post 方法,或者通过 img、iframe 等标签加载的 URL,这些场景都不会携带 Cookie。
- 而如果使用 None 的话,在任何情况下都会发送 Cookie 数据。
2. 验证请求的来源站点
Origin 属性只包含了域名信息,并没有包含具体的 URL 路径,这是 Origin 和 Referer 的一个主要区别。在这里需要补充一点,Origin 的值之所以不包含详细路径信息,是有些站点因为安全考虑,不想把源站点的详细路径暴露给服务器。
因此,服务器的策略是优先判断 Origin,如果请求头中没有包含 Origin 属性,再根据实际情况判断是否使用 Referer 值。
3. CSRF Token
跨域
CORS
这就全是服务端的工作了,主要的三个参数
Access-Control-Allow-Origin: 服务器可接受的请求来源
Access-Control-Request-Method: 服务器实际请求所使用的 HTTP 方法
Access-Control-Request-Headers: 服务器实际请求所携带的自定义首部字段。
代理接口
在本地调试开发还可以利用webpack + webpack-dev-server代理接口跨域。
需要使用本地开发插件:webpack-dev-server
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://xxx.com/',
pathRewrite: {'^/api' : ''},
changeOrigin: true, // target是域名的话,需要这个参数,
secure: false, // 设置支持https协议的代理
}
}
}
}
nginx反向代理
实现原理类似于中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。
其他
- websocket
- JSONP(只支持get请求)
- window.postMessage
- document.domain
- window.name+iframe
- location.hash+iframe
错误监控、埋点
工程化
Vue
Vue源码
《Vue.js设计与实现》
Vue写过的亮点
- AutocompleteCellType;Vue.extend;new Component({propsData}).$mount(editorContext.firstChild)
- APS 拖动,写法使用directive
- 多端sdk
- 右键菜单,做成directive;
- 树状node,jsx
Array是如何劫持
Object.defineProperty是可以劫持数组下标,但是将值pop出去,重新push,新值无法被监听,除非每次push、unshift监听他,重新劫持。性能消耗太大。而且arr[新下标]、arr.length=x无法被监听. segmentfault.com/a/119000001…
Vue3与Vue2变了什么,为什么这么改
JS基础
书籍:《红宝书》、《你不知道的JS》
作用域链
function out(){
console.log(a)
}
function f1() {
var a = '变量1'
var f2 = function () {
out()
}
return f2
}
var fn = f1()
fn()
// ReferenceError: a is not defined
创建对象
- 工厂模式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
- 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
要创建Person的实例,应使用new操作符。以这种方式调用构造函数会执行如下操作。(1)在内存中创建一个新对象。 (2)这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。 (3)构造函数内部的this被赋值为这个新对象(即this指向新对象)。 (4)执行构造函数内部的代码(给新对象添加属性)。 (5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。 3. 原型模式
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.sayNameAge = function() {
console.log(this.name,this.age);
};
let person1 = new Person();
person1.sayNameAge(); // "Nicholas",29
let person2 = new Person();
person2.sayNameAge(); // "Nicholas",29
person1.age+=1;
person1.sayNameAge(); // "Nicholas",30
person2.sayNameAge(); // "Nicholas",29
console.log(person1.sayNameAge == person2.sayNameAge); // true
Object.getPrototypeOf(person1).name="Derrick";
person1.sayNameAge(); // "Derrick",30
person2.sayNameAge(); // "Derrick",29
只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为null,也不会恢复它和原型的联系。不过,使用delete操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
hasOwnProperty方法可用作判断某个属性是在实例上还是原型对象上。in操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上
Object.create(obj)
继承
1. 原型链继承
存在问题:1⃣原型中包含引用类型,会在所有实例间共享;2⃣️子类型在实例化时不能给父类型的构造函数传参
2. 盗用构造函数
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
//继承SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red, blue, green, black"
let instance2 = new SubType();
console.log(instance2.colors); // "red, blue, green"
通过使用call()(或apply())方法,SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了。这相当于新的SubType对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的colors属性。
传递参数
function SuperType(name){
this.name = name;
}
function SubType() {
// 继承SuperType并传参
SuperType.call(this, "Nicholas");
// 实例属性
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); // 29
在这个例子中,SuperType构造函数接收一个参数name,然后将它赋值给一个属性。在SubType构造函数中调用SuperType构造函数时传入这个参数,实际上会在SubType的实例上定义name属性。为确保SuperType构造函数不会覆盖SubType定义的属性,可以在调用父类构造函数之后再给子类实例添加额外的属性。
问题: SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了。这相当于新的SubType对象上运行了SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的colors属性。
3. 组合继承
组合继承(有时候也叫伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。来看下面的例子:
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red, blue, green, black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red, blue, green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
存在问题,属性是挂载在实例上
原型式继承
object.create
寄生式继承
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
适合主要关注对象,而不在乎类型和构造函数的场景
寄生式组合继承
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
es6的extends
class SubType extends SuperType{
constructor(name,age){
super(name);
this.age=age
}
sayHi(){console.log("Hi")}
}
babel的语法糖
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
各种方式对比
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
function inheritPrototype2(subType, superType) {
let prototype = new superType(); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}
function SuperType(name) {
this.name = name||123;
this.colors = ["red", "blue", "green"];
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
function SubType2(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
inheritPrototype2(SubType2, SuperType);
class SubType3 extends SuperType{
constructor(name,age){
super(name);
this.age=age;
}
}
a=new SubType('w',12);
b=new SubType2('x',45);
c=new SubType3('y',45);
console.log(a.name,b.name,c.name)
delete a.name;
delete b.name;
delete c.name;
console.log(a.name,b.name,c.name)
console.log(SubType.prototype.__proto__)
console.log(SubType2.prototype.__proto__)
console.log(SubType3.prototype.__proto__)
垃圾回收机制
内存泄露
- 意外的全局变量
- 使用不当的闭包
- 遗忘的定时器
- 遗漏的DOM
- 循环引用
- 事件未正确解绑
- 有问题的第三方库
性能调优
代码优化
先来说说代码优化;代码优化 又可以分为 编译时优化 和 运行时优化 。
编译优化
编译时优化
1、加载目标文件(I/O操作非常耗时)。
2、解析文件生成AST。
3、代码转换,比如:将 JSX / TS转换为 JS, ES Next 转换为 es5,less / scss 转换为 css。
4、生成目标代码,比如:CMD、AMD、CommonJS、压缩、混淆等
编译时优化 目的是减少 I/O、代码转换、目标代码输出三个步骤的时间,提高构建效率,提升研效;
webpack:
1、合理配置resolve的 modules、extensions、mainFiles、 alias缩小文件的搜索范围。
2、loader 通过 test、exclude、include配置缩小文件的搜索范围。
3、使用DllPlugin减少依赖库的编译,只需编译一次,只要版本不升级就不需要再次编译。
4、使用HappyPack开启多线程打包。
5、其他的还有缓存、 优化压缩时间等。
vite使用ESBuild
1、减少文件查询读取(I/O)时间;
2、提升编译效率;
3、减少不必要的编译;
4、算法优化,提升cpu、内存等的利用。
运行时优化
运行时 可以考虑的点就比较多了,我们以浏览器 渲染管线 为例,
渲染管线 的核心工作是:
1、解析html、css生成dom树和样式层级表,过程中遇到
<script>则解析并执行js
2、计算出真正需要渲染的节点以及其元素大小、位置(这里计算耗费巨大)
3、分层,一些节点可能需要单独的图层
4、绘制,生成绘制指令表
5、提交给合成线程
可以看到,渲染进程 核心工作涵盖html、css、js,所以 运行时优化 主要考虑以下几点:
1、算法运行时效率,由于js运行时是单线程,如果程序长时间占用线程执行没法响应用户行为,体验会非常差;
2、精简的代码,比如:打包编译时使用Tree Shaking,减少运行时浏览器解析编译时间;
3、合理的代码,比如:尽量使用函数式编程范式进行编程,可以让引擎比如v8复用已经编译好的代码;
4、dom操作优化,css 优化等,减少重绘、重排等需要重新计算和绘制但可能又非必要的操作。
针对这4个优化点我们以当下主流的框架 Vue 和 React 来看看优化手段。
1、相同点
1、都是数据驱动,使用了
vnode和diff
2、React和vue3都迈向Hooks,优化逻辑复用
2、不同点 Vue2 到 Vue3
1、数据劫持优化,
Object.defineProperty改为Proxy,减少无脑递归 2、渲染优化,Vue3在编译阶段对模板进行分析,将静态节点生成Block tree,减少diff范围并且vnode更新可由模块减少为动态内容数量大小 3、Options API到Composition API,提升逻辑处理能力
React
1、用户可以利用生命钩子自定义
diff策略;
2、React 16引入Fiber,核心理念是利用帧空余调度任务执行,适时释放渲染进程占用,同时使用启发式更新算法,可以指定任务更新优先级;
3、事件监听优化,旧版React会把事件监听代理到document,这会导致页面的事件都会触发事件回调,导致渲染管线的流程走一遍,React 17则是代理到渲染树根DOM中;
4、React 17优化启发式更新算法,简单讲就是设定一个更新区间,并且动态的向区间中增减优先级,这样可以处理更细颗粒度的更新;
5、React 18并发渲染机制(Concurrent Rendering),主要是调度流程的优化
网络优化
代码优化 是研发人员相对可控的一个方向,而 网络优化 则相对是不太可控的一个方向,只能根据协议和硬件设施的情况选择综合效益最大的优化;网络优化 主要考虑有以下几点:
- 寻址:dns 花费时间从十几毫秒到上百毫秒不等;
- 静态文件访问速率:静态文件都可以缓存在 CDN 上,不需要每次都从源站获取;
- 传输效率:不同协议以及协议版本对带宽的利用率不同;
- 代理服务器:可以处理协议转换,短时缓存,加速访问等问题;
- 负载均衡:防止单点压力过大导致崩溃。
这么多个优化点,我们可以理解为两个方向:一个是负责内容传输的,一个是负责内容生产;对应的可以参考OSI七层模型去思考,这里就将 网络优化 理解为如下两个方向:
网络传输协议,主要以HTTP协议为例Server 端,以Nginx、DNS、CDN为例
vite、webpack优化
- 使用cdn、
externals - 生产环境中移除log
- gzip
- 图片压缩
- 首屏 动态引入、懒加载 和 预加载
- 多进程打包
- tree shaking
- 代码压缩
minimize - DLL动态链接库
- splitChunks