进程与线程
为什么要有进程
-
进程(process)是 CPU 资源(内存,文件)分配的基本单位
进程是操作系统的基础。一个应用程序可以有多个进程(浏览器,一个标签页一个进程,空标签页可以合并进程)。
-
进程之间相互独立,资源(内存)隔离,互不干扰
任一时刻,一个 CPU 总是运行一个
进程,其他进程处于非运行状态,CPU 使用时间片轮转进度算法来实现多个进程交替执行。多核 CPU 可以同时运行多个
进程,多进程可以充分利用多核CPU的性能,可用于多机多核分布式,易于扩展。 -
进程拥有自己的生命周期和状态
一个进程会独占一批资源,比如使用寄存器,内存,文件等。切换进程时,需要保存上下文(执行的中间结果,存放在内存中的程序的代码和数据,执行栈、通用目的寄存器的内容、程序计数器、环境变量以及打开的文件描述符的集合)。
创建状态(new)、就绪状态(ready)、就绪挂起状态、运行状态(running)、阻塞状态(blocked)、阻塞挂起状态、结束状态(exit)。
-
进程之间可以通信
IPC,如命名管道,匿名管道,socket,信号量,共享内存,消息队列等。
当某一组进程的每个进程均等待此进程中其他进程所占有的资源,而不释放自己本身所占有的资源,导致进程最终永远都无法得到所申请的资源,这种现象就称为死锁。
形成死锁的四个必要条件:
- 互斥条件,对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。
- 请求与保持条件,因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
- 不剥夺条件,已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件,发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞。
避免死锁,只需破坏产生死锁的四个条件中至少一个
- 破坏互斥条件(不可行),锁机制本身就是互斥的。
- 破坏请求与保持条件,一次性申请所有资源。
- 破坏不剥夺条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件,按某一顺序申请资源,释放资源则反序释放,避免循环等待。
lsof -i :8088 查看进程
kill pid 杀死进程
为什么要有线程
-
线程(thread)是 CPU 资源调度的基本单位。
进程是线程的容器,一个进程包含多个线程,一个线程只属于一个进程。
-
线程之间可以共享进程资源、通信,也就是可以并发运行且共享相同的地址空间,在空间效率和时间效率上更高效。
JS 和 Node 都是单线程,单线程的优点是,不用像多线程编程一样考虑死锁问题,但也存在缺点:
- 无法利用多核 CPU。
- 当进程中的一个线程奔溃时,会导致进程内的所有线程都错误退出,健壮性值得考验。
- 大量计算占用 CPU 导致无法继续调用异步 I/O。
为了解决单线程计算量大和异步的问题,浏览器引入了 Web Worker,Node 提出了 child_process,以及相应的事件循环机制。
为什么要有协程
协程是应用层面的,不再交由操作系统去切换处理,由开发者自己控制,减少上下文切换开销。
数据库事务的四大特性(ACID)
-
Atomicity 原子性
事务是一个不可分割的工作单位,要么发生,要么都不发生。
-
Consistency 一致性
事务前后数据额完整性必须保持一致。
-
Isolation 独立性
类似于进程的独立性。
-
Durability 持久性
事务的改变是永久的。
浏览器的组成结构
* 用户界面
* 主进程
* 内核
* 渲染引擎(Firefox:Gecko 引擎 -moz
Safari:WebKit 引擎 -webkit
Chrome/opera:Blink 引擎
IE: Trident 引擎 -ms
Edge: EdgeHTML 引擎)
* JS 引擎
* 执行栈
* 事件触发线程
* 消息队列
* 微任务
* 宏任务
* 网络异步线程
* 定时器线程
多进程
-
Browser进程,负责协调、主控的主进程;
-
第三方插件进程
-
GPU进程,最多一个,用于3D绘制等;
-
浏览器渲染进程(内核,Render进程)
在浏览器中新打开一个网页相当于开起了一个进程(有时候浏览器会合并多个进程),流畅但是吃内存
多线程
浏览器的多线程
浏览器的渲染进程是多线程的。
- GUI 渲染线程
负责HTML和CSS代码的解析,构建DOM 和 CSSOM。
与JS引擎线程互斥(JS可以操作DOM,可能会产生不一致或循环等待),当JS引擎线程执行时GUI线程会被挂起。
详见渲染机制。
- JS 引擎线程
负责解析JS脚本,例如V8引擎。
JS引擎一直等待任务队列中任务的到来,一个Tab页进程中只有一个JS线程在运行JS程序。
由于JS引擎线程与GUI渲染线程互斥,JS脚本的执行时间过长时会阻塞页面渲染。
- 事件触发线程
负责控制事件循环,归属于浏览器而不是JS引擎。
当JS引擎执行异步操作时(如AJAX请求),会将对应任务添加到事件线程。
当对应任务符合触发条件被触发时,该线程把事件回调添加到任务队列的队尾,等待JS引擎的处理(因为JS引擎是单线程)。
- 定时器线程
由于JS引擎是单线程,如果处于阻塞线程状态会影响定时的准确,因此通过单独线程来计时并触发定时。
- 网络异步线程
当XMLHttpRequest连接后,通过浏览器新开一个线程请求。
检测到请求状态变更时,如果有回调函数,异步线程就产生状态变更事件,并将事件放入事件队列,等到JS引擎执行。
Node 的多线程:
-
主线程:编译、执行代码。
-
编译/优化线程:在主线程执行的时候,可以优化代码。
-
分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据。
-
垃圾回收线程。
事件循环
JS是单线程(设计之初未曾想浏览器会变得复杂),任务分为同步任务和异步任务;同步任务在主线程上执行,执行上下文(execution context)依次被压入执行栈(Call Stack),来保证代码的有序执行,可分为全局执行上下文和函数执行上下文;遇到异步任务,JS引擎不会一直等待,而是继续进行执行栈;事件触发线程管理着任务队列,当异步任务有了运行结果,就把异步任务的回调加入任务队列。
当执行栈里没有任务在执行,即JS引擎空闲时,从任务队列中取出回调,压入执行栈,到主线程执行;每个宏任务里都有微任务队列,当前宏任务执行完成,首先判断微任务对列中是否为空,如果不为空就将微任务的事件压入栈中执行,直到当前微任务对列为空,再去处理下一个宏任务。
Node 中的事件循环有所不同。浏览器中的微任务是在每个相应的宏任务中执行的,而 Node 中的微任务是在不同阶段之间执行的。
宏任务
script脚本, setTimeout, setInterval, setImmediate, I/O事件,UI渲染。
浏览器为了使得 JS 内部任务与 DOM 任务能有序执行,会在一个 task 执行结束后、下一个 task 执行开始前,对页面进行重渲染。(?)
微任务
promise.then(),MutationObserver,node 中的 process.nextTick,V8 的垃圾回收。
在渲染之前执行,微任务之后可能会发生渲染(Vue中nextTick的异步)。
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
newPromise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
/**
* script start => async2 end => Promise => script end =>
* promise1 => promise2 => async1 end => setTimeout
* 理论上 async1 的输出应该在微任务队列的末尾,但实际上浏览器优化会在 promise1 和 promise2 之间输出。
*/
执行栈
栈:先进后出的结构;
- 全局执行上下文
- 函数执行上下文
- eval执行上下文
当 JS 引擎开始执行代码,首先产生全局执行上下文并压入执行栈,每遇到一个函数调用就往栈中压入一个新的函数执行上下文,ESP 指针指向栈顶来保存当前的执行状态。JS 引擎执行栈顶的函数,执行完毕则弹出当前的执行上下文(包括涉及的变量和 this 指向等)。
var count = 0;
function foo(count) {
count += 1;
console.log(count);
}
foo(count); // 1
foo(count); // 1
var count = 0;
/* 函数没有指定形参,则会到全局作用域里找,被当做全局变量 */
function foo() {
count += 1;
console.log(count);
}
foo(count); // 1
foo(count); // 2
渲染机制
- 解析 HTML 代码,构建 DOM 树
这个过程涉及抽象语法树(AST)和webkit的相关知识,DOM 树构建完成是 DOMContentLoaded(ready)状态。
渲染引擎与 JS 引擎线程互斥,所以建议 JS 代码放在 HTML 代码之后,也可以设置 defer 或 async 属性。
<script>的 src属性,表示是资源,下载资源会挂起 HTML 渲染。
<script>的 defer:适用于与 DOM 有关,在网络请求线程里下载完成后,当整个 document 解析完毕后再执行脚本文件,在 DOMContentLoaded 事件触发之前完成。多个脚本按顺序执行。
<script>的 async:适用于与 DOM 无关,在网络请求线程里下载完成后,挂起 HTML 解析,执行 JS。多个脚本的执行顺序无法保证。
- 解析 CSS 代码(内部样式,外部样式,内联样式),生成 CSS 规则树(CSS Object Model,CSSOM)
<link>的 href 属性,表示是超文本引用,并行下载,不会阻塞 HTML。
<link>的 preload:声明式 fetch,一般是当前页面需要的资源,告诉浏览器预先请求当前页面需要的资源(关键的脚本,字体,图片等),可以在不阻塞 onLoad 事件的情况下请求资源。
<link>的 prefetch:预读取,一般是其他页面可能需要的资源,在 Chrome 中如果用户导航离开一个页面,而对其他页面的prefetch请求仍在进行中,这些请求将不会被终止(网络堆栈缓存)。
- 合并 DOM 树和 CSS 规则树,生成渲染树 render tree
主要是排除非视觉节点,比如 script,meta 标签和 display 为 none 的节点)。
- 布局渲染树 (reflow / 回流 )
从根结点递归调用,计算每个元素的大小和位置等,给出每个结点应该在屏幕上的精确坐标
- 绘制渲染树 (repaint / 重绘)
遍历渲染树,使用GPU绘制每个结点,将各图层合成(composite,普通图层和复合图层),显示在屏幕上。
以上并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,可能存在:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。
样式闪烁(FOUC)
-在 CSS 加载之前,HTML 已经解析完毕,就会导致展示出无样式内容,然后突然闪现出样式的情况。
这是因为CSS文件的加载时间过长,可能放在了底部。
白屏
CSS 放在 HTML 后面,CSS 文件迟迟加载不出来,导致浏览器没有渲染。
JS 文件的加载或执行阻塞了 HTML 文档的解析。
重绘和回流
-
重绘 (repaint 或 redraw)
当元素的可见外观发生改变,但并没有影响到大小和位置的时候,浏览器会根据元素的新属性重新绘制。
-
回流 / 重排 (reflow)
增删 DOM 元素,当页面或 DOM 元素的大小或位置(几何属性)发生改变,浏览器需要重新计算布局渲染树。
每个页面至少需要一次回流,就是在页面首次渲染的时候。
回流的性能消耗也是操作 DOM 开销大的原因。
回流必定会引发重绘,但重绘不一定会引发回流。
减少回流:
- DOM 操作尽量一起执行(虚拟DOM的一大优点)。
- 缓存 DOM 信息,使用
documentFragment对象在内存里操作 DOM。 - 用 Class 一次性改变样式,而不要用 Style 逐项地改变样式。
- 动画使用
absolute或fixed定位(脱离标准文档流),这样可以减少对其他元素的影响。 - 用
visibility: hidden代替display: none实现隐藏元素。 - 对窗口的 resize 事件做防抖或节流处理
GPU加速
优点:使用 transform、opacity、filters 等属性时,会直接在 GPU 中完成处理,这些属性的变化不会引起回流重绘。
缺点:GPU 渲染字体会导致字体模糊,过多的GPU处理会导致内存问题。
window.onload() / document.ready()
window.onload(),所有元素(包括大量的图片等)加载完成后才能进行后续操作,只能执行一次。
document.ready(),即DomContentLoaded,DOM 树绘制完成后就可进行相关操作,可执行多次,但是可能部分属性还未生效。
地址栏输入URL后?
- DNS解析
浏览器缓存 =》 操作系统缓存 =》 本地 DNS 缓存 (
/etc/hosts) =》本地 DNS 服务器 =》根 DNS服务器 =》顶级 DNS服务器 =》权威 DNS服务器,得到该URL对应的服务器IP和端口。
- TCP连接
TCP 三次握手
- 发送HTTP请求
浏览器判断所请求的资源是否在缓存或者缓存是否过期。
如果是 HTTPS 请求,在通信前进行 TLS 四次握手。
构造并封装 HTTP 请求在一个 TCP 数据包里,传输层-》网络层-》数据链路层-》物理层-》服务器。
- 服务器处理请求并返回HTTP报文
服务器返回响应的 HTML 页面给浏览器,可能是经过压缩的。
- 浏览器解析渲染页面
构建DOM树和CSS规则树(遇到script标签,渲染挂起)-》合成渲染树-》布局渲染树-》绘制渲染树
- 连接结束
TCP四次挥手
首屏 / 白屏时间
- 首屏时间
Native WebView: onload ;
IOS:webViewDidFinishLoad;
Android: onPageFinished;
- 白屏时间
没有任何内容是白屏:判断DOM节点数少于某个阈值
网络或服务异常是白屏:判断出现业务定义的错误码
图片加载错误是白屏
Web Worker
通过类似定时器、回调函数等异步编程方式在平常工作中已经足够,但是如果做复杂运算,不足就逐渐体现出来,比如 setTimeout拿到的值并不正确,或者页面有复杂运算的时候很容易触发假死状态,异步代码会影响主线程的代码执行,异步终究还是单线程,不能从根本上解决问题。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。等到子线程完成再通过 postMessage(拷贝传递)返回结果给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担,主线程(通常负责UI交互)不会被阻塞或拖慢。
web worker的一般使用
/**
* 检测浏览器对于web worker的支持性
*/
if (window.Worker) {
...
}
/**
* 创建web worker文件(js,回传函数等)
* 并通过构造函数创建web worker对象
*/
let myWorker = new Worker('worker.js');
/**
* worker通过postMessage() 方法和onmessage事件进行数据通信
* 主线程和子线程是双向的,都可以发送和监听事件。
*/
myWorker.postMessage('hello, world'); // 发送
worker.onmessage = function (event) { // 接收
console.log('Received message ' + event.data);
doSomething();
}
function doSomething() {
// do
worker.postMessage('work done')
}
/**
* 当子线程使用完毕,为了节省系统资源,可以手动关闭。
* 如果worker没有监听消息,那么当所有任务执行完毕(包括计时器)后,会自动关闭。
*/
// in main thread
worker.terminate();
// in worker thread
close();
web worker的使用注意
-
同源限制
-
DOM限制
无法获取主线程的DOM(即无法使用document、window、parent等)
-
global对象限制
无法使用document 等全局对象,指向有变更,window需要改写成self,不能执行alert()和confirm()等方法。
-
使用异步
worker 子线程可以使用 XMLHttpRequest 发出 AJAX 请求,可以使用 websocket 进行持续连接,可以使用 setTimeout() 等,通过 import (url) 加载另外的脚本文件,但不能跨域。
web worker的使用场景
主要是大数据处理等耗时任务,相当于一个nodejs级别的运行环境。
- 数学运算
处理ajax返回的大批量数据; 读取用户上传文件; 计算MD5; 更改canvas位图的过滤,分析音视频文件等;
-
高频用户交互
-
数据的预取和缓冲**
Shared Worker
不同于 web worker, Shared Worker 是浏览器所有页面共享的,不能采用与 Worker 同样的方式实现,因为它不隶属于某个渲染进程,可以为多个渲染进程共享使用。
Service Worker
Service Worker 只是一个常驻在浏览器中的 JS 线程,常用于离线缓存。
Service Worker 类似一个拦截层,可以拦截页面发起的请求,拦截后,可以去访问本地缓存。
- 与 Fetch 搭配,可以从浏览器层面拦截请求,做数据 mock;
- 与 Fetch 和 Cache Storage 搭配,可以做离线应用;
- 与 Push 和 Notification 搭配,可以做像 Native APP 那样的消息推送;
- 综合Manifest(桌面图标)使用,PWA应用
使用注意:
- 不能访问Dom;
- 运行在独立线程中,不会阻塞主线程;
- 完全异步,同步内容不能使用;
- 必须使用 https 协议;
内存
内存的类型
-
栈内存
栈内存存储的是基础类型变量以及一些对象的引用变量(即地址,这也是修改引用类型变量总会影响到其他指向该地址的变量的原因),占据固定大小的空间; 由操作系统自动分配内存空间,自动释放,JS 引擎会通过向下移动 ESP(记录当前执行状态的指针)来销毁保存在栈中的执行上下文(全局执行上下文、函数执行上下文),也就是切换上下文。
-
堆内存
堆内存中的对象不会随方法的结束而被销毁,创建对象是为了反复利用(因为对象的创建成本通常较大),这个对象将被保存到运行时数据区(也就是堆内存)。只有没有任何引用变量引用该对象时时,系统的垃圾回收机制才会在核实的时候回收它。 由操作系统动态分配的内存,大小不定也不会自动释放,一般由程序员分配释放(C/C++),也可由垃圾回收机制回收。
内存泄漏
原因:不再用到的变量一直在内存里,没有及时回收。
- 大容量缓存
- 意外的全局变量;
- 被遗忘的计时器或回调函数;
- 闭包的滥用;
- 已分离的DOM节点(某个节点已从 DOM 树移除,但某些 JavaScript 仍然引用它);
- 事件重复监听;
排查方法:
- 使用 Chrome 任务管理器实时监视内存使用,查看内存占用空间(原生DOM内存)和JavaScript使用的内存(JS堆内存);
- 利用chrome 时间轴记录可视化内存泄漏,开始录制前先点击垃圾回收-->点击开始录制-->点击垃圾回收-->点击结束录制:
- 使用堆快照排查DOM Tree的内存泄漏;
如何避免内存泄漏:
- 少使用全局变量;
- 慎用闭包
- 计时器回调,不用时及时销毁;
- 使用WeakSet WeakMap (弱引用,不计入垃圾回收机制);
垃圾回收机制
JS 引擎只能使用系统的一部分内存。具体来说,在64位系统下,V8最多只能分配 1.4G;在 32 位系统中,最多只能分配 0.7G。
为什么要设置内存上限?垃圾回收过程非常耗时,执行垃圾回收机制会使得JS引擎挂起,因此V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,称为增量标记(Incremental Marking)算法。
V8引擎把堆内存分为:代码区、Map区、大对象区(large object space)、新生代(new space)、老生代(old space)
新生代内存
生存时间短,副垃圾回收器。
Scavenge GC 算法,From(正在使用的空间)和To(闲置的空间),检查 From 空间内的对象,把还在被引用的对象按照顺序复制到 To 空间(这样设计是为了处理内存碎片),非活动对象直接回收,From 和 To 空间角色对调;
老生代内存
存时间长,主垃圾回收器
- 标记清除(Mark-Sweep)算法
引用计数:给一个变量赋值引用类型,则该对象的引用次数+1,如果这个变量变成了其他值,那么该对象的引用次数-1。垃圾回收器会回收引用次数为0的对象。但是当对象循环引用时,会导致引用次数永远无法归零,造成内存无法释放。
标记清除:垃圾收集器先给内存中所有对象加上标记,然后从根节点开始遍历,去掉被引用的对象和运行环境中对象的标记,剩下的被标记的对象就是无法访问的等待回收的对象。
- 标记整理(Mark-Compact)算法(清理内存碎片)
频繁回收对象后,可能会存在大量不连续内存空间(内存碎片),在清除阶段结束后,> V8把存活的对象全部往一端靠拢。
前端内存使用注意(以地图标注为例):
-
在数据量不是很大的情况下,选择操作更加灵活的普通数组;
-
在大数据量下,选择一次性分配连续内存块的类型数组或者 DataView;
-
不同线程间通讯,数据量较大时采用 sharedBufferArray 共享数组;
-
使用 Memory来检测是否存在内存问题,了解了垃圾回收机制,减少不必要的 GC 触发的 CPU 消耗。
前端监控
数据监控
- PV(Page View)页面访问量 / UV(User View)IP地址访问人数
- 用户在页面的停留时间
- 用户的访问入口
- 用户的功能点击量统计
性能监控
- 首屏时间(不同用户、设备、系统)
- 白屏时间
- HTTP等待相应的时间
- 静态资源下载时间
- 页面渲染时间
异常监控
- 资源加载异常
- JS异常监控
- 样式丢失异常监控
埋点
- 无埋点
记录所有事件,定期上传快照,数据传输的压力较大,代表方案是国内的GrowingIO;
- 可视化埋点
通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据,从而实现所谓的“无痕埋点”,代表方案是已经开源的Mixpanel;
- 代码埋点
在实际项目中考虑到上报数据的灵活定制,以及减少数据传输和服务器的压力,在所需埋点处不多的情况下,常用的方式是代码埋点,但是埋点代码与业务代码耦合,存在后期维护的问题。 如果埋点的事件不多,可以事件触发后立即上报;如果埋点的事件较多,或者说网页内部交互频繁,可以通过本地存储的方式先缓存上报信息,然后定期上报。
比如,当用户进入页面,在 DOMContentLoaded(DOM挂载完成)或 onLoad事件(所有资源加载渲染完毕)里插入代码,获取用户的机型、系统,访问信息(统计PV / UV),首屏时间,访问来源(History对象)等信息,并发送给服务端。
比如,当用户跳转路由的时候,可在 hashchange, history.popstate, 导航守卫事件里插入代码,计算页面停留时间,关闭应用可通过 onunload事件监听。
- who
appId(系统或者应用的id),userAgent(用户的系统、网络等信息)
- when
timestamp(上报的时间戳)
- where
currentUrl(哪一个页面跳转到当前页面),type(上报的事件类型),element(触发上报事件的元素)
- what
上报的自定义扩展数据data:{},扩展数据中可以按需求定制,比如包含uid等信息
- how
声明式埋点
将埋点代码和具体的交互和业务逻辑解耦,开发者只用关心需要埋点的控件,并且为这些控件声明需要的埋点数据即可,从而降低埋点的成本。 埋点问题不能通过单一技术方案来解决,在不同场景下选择不同的埋点方案。
性能优化
这是个很大的话题,主要可以从用户体验、前端工程化、SEO、维护等几个角度扩散来说
- 延迟和带宽对 Web 性能的影响
- 传输协议(TCP)对 HTTP 的限制
- HTTP 协议自身的功能和缺陷
- Web 应用的发展趋势及性能需求
- 浏览器局限性和优化思路
用户体验
从输入URL到页面渲染完成的过程中来说,减少首屏时间( onLoad事件),等待过程中可设置骨架屏。
- 加快请求速度
DNS缓存,DNS预解析,减少域名数,负载均衡。
CDN分发(负载均衡,对静态资源的请求不必包含cookie验证等)。
gzip压缩。
- 减少请求数量
减少HTTP请求数,HTTP2.0多路复用。
按需加载(分块打包),图片(webP,base64,雪碧图,懒加载)。
避免 HTTP 重定向,需要重新建立连接。
- 缓存
缓存机制(强缓存、协商缓存),图片缓存,离线缓存manifest,PWA
- 渲染
服务器端渲染,实现秒开和 SEO,浏览器对象需要重写,某些生命周期函数不可用。
客户端渲染,样式在上,脚本在下,script 脚本可能会阻塞渲染,因此建议把 script 标签放在 HTML 代码后面,或者设置 defer/ async 属性异步下载;CSS 文件可以并行加载,建议放在 head 标签内使用 link;尽量减少渲染树的重排。
前端工程化
开发流程
-
语言增强
-
代码校验
html:语义标签,变量名决不妥协。
CSS:使用选择器的时候,尽量避免通配符和嵌套过深(性能开销大);尽量使用class改变样式,而不要注意改变;尽量减少页面回流...
JS:函数防抖、节流;事件委托;组件缓存;善用 React 和 Vue 里的key...
打包编译流程
- 提高打包速度
多进程打包,缩小打包作用域(提高文件查找的速度)。
- 减少打包的整体体积
压缩代码,压缩图片资源; Tree Shaking
- 分块打包
分离第三方库和业务代码(方便缓存资源),source-map优化;
- 文件指纹与版本控制
上线与后期流程
预渲染,服务器端渲染,SEO优化