前端--浏览器

609 阅读24分钟

进程与线程

为什么要有进程

  • 进程(process)是 CPU 资源(内存,文件)分配的基本单位

    进程是操作系统的基础。一个应用程序可以有多个进程(浏览器,一个标签页一个进程,空标签页可以合并进程)。

  • 进程之间相互独立,资源(内存)隔离,互不干扰

    任一时刻,一个 CPU 总是运行一个进程,其他进程处于非运行状态,CPU 使用时间片轮转进度算法来实现多个进程交替执行。

    多核 CPU 可以同时运行多个进程,多进程可以充分利用多核CPU的性能,可用于多机多核分布式,易于扩展。

  • 进程拥有自己的生命周期和状态

    一个进程会独占一批资源,比如使用寄存器,内存,文件等。切换进程时,需要保存上下文(执行的中间结果,存放在内存中的程序的代码和数据,执行栈、通用目的寄存器的内容、程序计数器、环境变量以及打开的文件描述符的集合)。

    创建状态(new)、就绪状态(ready)、就绪挂起状态、运行状态(running)、阻塞状态(blocked)、阻塞挂起状态、结束状态(exit)。

  • 进程之间可以通信

    IPC,如命名管道,匿名管道,socket,信号量,共享内存,消息队列等。

当某一组进程的每个进程均等待此进程中其他进程所占有的资源,而不释放自己本身所占有的资源,导致进程最终永远都无法得到所申请的资源,这种现象就称为死锁。

形成死锁的四个必要条件:

  1. 互斥条件,对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。
  2. 请求与保持条件,因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件,已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件,发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞。

避免死锁,只需破坏产生死锁的四个条件中至少一个

  1. 破坏互斥条件(不可行),锁机制本身就是互斥的。
  2. 破坏请求与保持条件,一次性申请所有资源。
  3. 破坏不剥夺条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件,按某一顺序申请资源,释放资源则反序释放,避免循环等待。

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 引擎
           * 执行栈
       * 事件触发线程
           * 消息队列
               * 微任务
               * 宏任务
       * 网络异步线程
       * 定时器线程

多进程

  1. Browser进程,负责协调、主控的主进程;

  2. 第三方插件进程

  3. GPU进程,最多一个,用于3D绘制等;

  4. 浏览器渲染进程(内核,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 之间输出。
 */

执行栈

栈:先进后出的结构;

  1. 全局执行上下文
  2. 函数执行上下文
  3. 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

渲染机制

  1. 解析 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。多个脚本的执行顺序无法保证。

  1. 解析 CSS 代码(内部样式,外部样式,内联样式),生成 CSS 规则树(CSS Object Model,CSSOM)

<link>的 href 属性,表示是超文本引用,并行下载,不会阻塞 HTML。

<link>的 preload:声明式 fetch,一般是当前页面需要的资源,告诉浏览器预先请求当前页面需要的资源(关键的脚本,字体,图片等),可以在不阻塞 onLoad 事件的情况下请求资源。

<link>的 prefetch:预读取,一般是其他页面可能需要的资源,在 Chrome 中如果用户导航离开一个页面,而对其他页面的prefetch请求仍在进行中,这些请求将不会被终止(网络堆栈缓存)。

  1. 合并 DOM 树和 CSS 规则树,生成渲染树 render tree

主要是排除非视觉节点,比如 script,meta 标签和 display 为 none 的节点)。

  1. 布局渲染树 (reflow / 回流 )

从根结点递归调用,计算每个元素的大小和位置等,给出每个结点应该在屏幕上的精确坐标

  1. 绘制渲染树 (repaint / 重绘)

遍历渲染树,使用GPU绘制每个结点,将各图层合成(composite,普通图层和复合图层),显示在屏幕上。

以上并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,可能存在:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。

样式闪烁(FOUC)

-在 CSS 加载之前,HTML 已经解析完毕,就会导致展示出无样式内容,然后突然闪现出样式的情况。

这是因为CSS文件的加载时间过长,可能放在了底部。

白屏

CSS 放在 HTML 后面,CSS 文件迟迟加载不出来,导致浏览器没有渲染。

JS 文件的加载或执行阻塞了 HTML 文档的解析。

重绘和回流

  • 重绘 (repaint 或 redraw)

    当元素的可见外观发生改变,但并没有影响到大小和位置的时候,浏览器会根据元素的新属性重新绘制。

  • 回流 / 重排 (reflow)

    增删 DOM 元素,当页面或 DOM 元素的大小或位置(几何属性)发生改变,浏览器需要重新计算布局渲染树。

    每个页面至少需要一次回流,就是在页面首次渲染的时候。

    回流的性能消耗也是操作 DOM 开销大的原因。

    回流必定会引发重绘,但重绘不一定会引发回流。

减少回流:

  1. DOM 操作尽量一起执行(虚拟DOM的一大优点)。
  2. 缓存 DOM 信息,使用documentFragment 对象在内存里操作 DOM。
  3. 用 Class 一次性改变样式,而不要用 Style 逐项地改变样式。
  4. 动画使用absolutefixed定位(脱离标准文档流),这样可以减少对其他元素的影响。
  5. visibility: hidden 代替 display: none 实现隐藏元素。
  6. 对窗口的 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的使用注意

  1. 同源限制

  2. DOM限制

    无法获取主线程的DOM(即无法使用document、window、parent等)

  3. global对象限制

    无法使用document 等全局对象,指向有变更,window需要改写成self,不能执行alert()和confirm()等方法。

  4. 使用异步

    worker 子线程可以使用 XMLHttpRequest 发出 AJAX 请求,可以使用 websocket 进行持续连接,可以使用 setTimeout() 等,通过 import (url) 加载另外的脚本文件,但不能跨域。

web worker的使用场景

主要是大数据处理等耗时任务,相当于一个nodejs级别的运行环境。

  1. 数学运算

处理ajax返回的大批量数据; 读取用户上传文件; 计算MD5; 更改canvas位图的过滤,分析音视频文件等;

  1. 高频用户交互

  2. 数据的预取和缓冲**

Shared Worker

不同于 web worker, Shared Worker 是浏览器所有页面共享的,不能采用与 Worker 同样的方式实现,因为它不隶属于某个渲染进程,可以为多个渲染进程共享使用。

Service Worker

Service Worker 只是一个常驻在浏览器中的 JS 线程,常用于离线缓存。

Service Worker 类似一个拦截层,可以拦截页面发起的请求,拦截后,可以去访问本地缓存。

  1. 与 Fetch 搭配,可以从浏览器层面拦截请求,做数据 mock;
  2. 与 Fetch 和 Cache Storage 搭配,可以做离线应用;
  3. 与 Push 和 Notification 搭配,可以做像 Native APP 那样的消息推送;
  4. 综合Manifest(桌面图标)使用,PWA应用

使用注意:

  1. 不能访问Dom;
  2. 运行在独立线程中,不会阻塞主线程;
  3. 完全异步,同步内容不能使用;
  4. 必须使用 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 空间角色对调;

老生代内存

存时间长,主垃圾回收器

  1. 标记清除(Mark-Sweep)算法

引用计数:给一个变量赋值引用类型,则该对象的引用次数+1,如果这个变量变成了其他值,那么该对象的引用次数-1。垃圾回收器会回收引用次数为0的对象。但是当对象循环引用时,会导致引用次数永远无法归零,造成内存无法释放。

标记清除:垃圾收集器先给内存中所有对象加上标记,然后从根节点开始遍历,去掉被引用的对象和运行环境中对象的标记,剩下的被标记的对象就是无法访问的等待回收的对象。

  1. 标记整理(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事件),等待过程中可设置骨架屏。

  1. 加快请求速度

DNS缓存,DNS预解析,减少域名数,负载均衡。

CDN分发(负载均衡,对静态资源的请求不必包含cookie验证等)。

gzip压缩。

  1. 减少请求数量

减少HTTP请求数,HTTP2.0多路复用。

按需加载(分块打包),图片(webP,base64,雪碧图,懒加载)。

避免 HTTP 重定向,需要重新建立连接。

  1. 缓存

缓存机制(强缓存、协商缓存),图片缓存,离线缓存manifest,PWA

  1. 渲染

服务器端渲染,实现秒开和 SEO,浏览器对象需要重写,某些生命周期函数不可用。

客户端渲染,样式在上,脚本在下,script 脚本可能会阻塞渲染,因此建议把 script 标签放在 HTML 代码后面,或者设置 defer/ async 属性异步下载;CSS 文件可以并行加载,建议放在 head 标签内使用 link;尽量减少渲染树的重排。

前端工程化

开发流程

  1. 语言增强

  2. 代码校验

html:语义标签,变量名决不妥协。

CSS:使用选择器的时候,尽量避免通配符和嵌套过深(性能开销大);尽量使用class改变样式,而不要注意改变;尽量减少页面回流...

JS:函数防抖、节流;事件委托;组件缓存;善用 React 和 Vue 里的key...

打包编译流程

  1. 提高打包速度

多进程打包,缩小打包作用域(提高文件查找的速度)。

  1. 减少打包的整体体积

压缩代码,压缩图片资源; Tree Shaking

  1. 分块打包

分离第三方库和业务代码(方便缓存资源),source-map优化;

  1. 文件指纹与版本控制

上线与后期流程

预渲染,服务器端渲染,SEO优化