简聊前端的技术原理和性能优化-tips

注:某次技术分享的素材

前端范围

chrome(市占率70%)上的web

移动端的webview/webkit

混合开发的React Native、Flutter

桌面端的electron ?(参考:VS Code 启动速度优化 - CovalenceConf 2019: Visual Studio Code – The First Second  ?v=r0OeHRUCCb4&ab_channel=ElectronUserland) 

 业务后端(nodejs)?

前端目标:拓展技术使用的业务边界

原理

简单的全链路流程:客户端-公网端-服务端

不同端有各自的问题和优化方法。比如公网端:

访问源站慢问题,出现CDN分流;

跨运营商网络问题,出现多运营商接入;

公网路由器跳数过多,出现专线;

google统计互联网资源文件大小,提高tcp初始化窗口大小,减少rtt次数;

客户端

web层:

html、css、js、image、font、audio、video

vue、React、RN、flutter、webpack、babel

CSR、SSR、NSR、PWA、wasm

NSR: native side render - 主要是app端使用

应用层-浏览器:chrome

--很多技术出自chromium,可以多多研究

http、ssl\tls

浏览器缓存一般分为两类:强缓存,也称本地缓存;以及弱缓存,也就是协商缓存。

请求一个资源时,会按照优先级(Service Worker -> Memory Cache -> Disk Cache -> Push Cache)依次查找缓存

Etag 跟服务器配置有关,每台服务器的 Etag 都是不同的

v8,jit(gc: 新生代+老年代)、aot、wasm、memory-pool、js-CodeCache(memory-buffer)、generated-code(序列号保存到磁盘)、snapshot、js编译引擎sparkplug加速(ignition解释器-sparkplug编译器-TurboFan优化器)

devtools:卡顿:帧率、耗时方法、耗时动作、render次数、冗余render、事件响应耗时、绘图指令延迟

浏览器缓存:

1. 强缓存:

cache-control: req 和response header 都可以带上:

max-age:即最大有效时间,在上面的例子中我们可以看到

must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。

no-cache:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的对比来决定。

no-store: 真正意义上的“不要缓存”。所有内容都不走缓存,包括强制和对比。

public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)

private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。

reponse header会根据情况带上:

last-modified: xxx

etag: 

2. 协商缓存(1 > 2):

    1. Etag & If-None-Match:

    2. Last-Modified & If-Modified-Since

3. 废弃了的expire等用法可以不用怎么管了

// nginx-1.15.8/src/http/ngx_http_core_module.cngx_int_tngx_http_set_etag(ngx_http_request_t *r){...etag->hash = 1;ngx_str_set(&etag->key, "ETag");etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3);if (etag->value.data == NULL) {etag->hash = 0;return NGX_ERROR;}//nginx 生成etag值etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"",r->headers_out.last_modified_time,r->headers_out.content_length_n)- etag->value.data;...return NGX_OK;}

chrome原理

有种说法: 

defer 的脚本被完全缓存时,并没有遵守规范等待解析结束,反⽽阻塞 了解析与渲染?

强制布局

正常布局:串行

强制布局:并行

布局抖动就是多次强制布局导致;

chrome js引擎

chrome js 代码缓存:

真实世界的数据显示,代码缓存命中率(对于可以缓存的脚本)很高(~86%)。虽然这些脚本的缓存命中率很高,但是我们每个脚本缓存的代码量并不是很高。我们的分析表明,增加缓存的代码量可以使 JavaScript 代码的解析和编译减少大约 40% 的时间。

https://v8.js.cn/blog/improved-code-caching/

gc算法:

JavaScript v8 gc:

1. 新生代和老年代;

2. 新生代:标记-复制;空间小,1-8M;Scavenge算法-分A-B区;

3. 老年代:对象大,时间长的放到老年代;多次增量子标记+一起清理;

语言的演化:

JavaScript-最开始作为玩具语言开发出来的,通过js引擎解释执行,速度慢,本身存在很多反常识的坑;

JIT - just in time通过缓存编译结果,直接执行机器码;但是JIT本身也会耗时;

AOT - ahead of time 预编译成机器码,js引擎可以直接运行,缺点是AOT文件过大;

GC -  解决原来需要手动分配和释放内存的方式,期望解决内存安全漏洞,提高开发效率,但GC可能带来stop the world的卡顿,GC本身也演化出各种算法。

TypeScript-采用面向对象的强类型方式通过AST 自动转换为JavaScript ,期望的是解决编码质量和效率问题;

WASM-其它语言源码编译成机器码,浏览器可以直接运行,解决js本身的解释型语言性能跟不上的问题。

OS:

Pid (抢占时间片) : 静态优先级、动态优先级+ 调度算法

CPU(核数+频率+缓存):CPU Cache Line(二维数组遍历)、分支预测、调度算法(CFS)、大小核插拔,cpu提频、taskset、cgroups

MEM:libc(malloc),buddy-system(page alloc),shrink(回收内存),

FS: page-cache(预热,读慢,写快),定时fsync、dirty-page% 

DISK: 电梯算法、firmware-cache

flash:SSD 、顺序读写、随机读写、WAL

NET: https,SSL\TLS、HTTP1.1 H2(header压缩,stream并发,push),h3(connect-id); tcp(fast open)、udp, tcp网络栈和NIC(lane)

power:高耗电应用限频

cpu侧:充分利用cpu cache line(64字节大小?)和 分支预测等 提高代码运行效率

TLS优化:

秘钥交换:基于椭圆曲线的 ECDH 密钥协商算法,

数据加密:AES-GCM (并行,cpu支持AES-NI)、CHACHA20算法

升级版本:TLS1.3 将握手时间从 2 个 RTT 降为 1 个 RTT

http 1.1 keep alive、response body 压缩

h2:header 压缩、stream并发(突破6个tcp限制)、server-push(解决rtt)

h3:connection-id:干掉tcp握手

request body: 业务自定义压缩:文本到二进制, json -> protobuff

队头阻塞问题...

这里需要说明的是:

h2的二进制分帧是不是说的对body进行二进制压缩。body是什么就还是什么,如果要对body压缩,response body还是需要开启gzip、br等;request body是可以自己定义的,和后端约定指定格式和压缩算法即可。

网络链路

DNS:跨运营商问题,骨干网

IP

CDN:边缘节点优化,

BGP - AnyCast

服务端

网关

LB

静态资源层:nginx、nodejs

动态资源:SSR、nodejs

微服务:无状态可扩展

数据层:cache、db、mq

docker

k8s

VIP

tcp优化参考:

慢启动

timeout

冲突算法

初始窗口大小

优化

为什么要做性能优化

C端产品:

Pinterest减少⻚⾯加载时⻓40%, 搜索和注册数提⾼了15% 

BBC⻚⾯加载时⻓每增加1秒,⽤户流失10% 

DoubleClick发现如果移动⽹站加载时⻓超过3秒,53%的⽤户会放弃

B端产品:

节约耗时打开次数用户量 * 薪资成本 = 降本增效

ROI-不要盲目优化: 

体验

口碑

练技术

性能优化,有时候不是让好的变得更好,而是让差的变好(用户分级、设备分级、网络分级)

用户对性能延迟的看法

0 至 16 毫秒

用户非常关注轨迹运动,他们不喜欢不流畅的动画。如果每秒渲染 60 个新帧,他们就认为动画很流畅。也就是说,每一帧只有 16 毫秒时间,这包括浏览器将新帧绘制到屏幕所需的时间,因而应用约有 10 毫秒的时间来生成一个帧。

0 到 100 毫秒

在此时间窗口内响应用户操作会让用户觉得结果是即时呈现的。如果时间更长,操作与用户反应之间的联系就会中断。

100 到 1000 毫秒

在此时间窗口内,用户会觉得任务进展基本上是自然连续的。对 Web 上的大多数用户来说,加载页面或更改视图是一项任务。

1000 毫秒或更长

超过 1000 毫秒(1 秒),用户的注意力就会从正在执行的任务上转移。

10000 毫秒或更长

超过 10000 毫秒(10 秒),用户会感到失望,并且可能放弃任务。他们以后可能会回来,也可能不会再回来。

注:用户对性能延迟的感知有所不同,具体取决于网络条件和硬件。

方法

分析完业务全链路流程后:

定指标-量化-监控-分析-优化-防退化

Web指标:

FP

FCP

FMP

DomContentLoaded

OnLoad

Performance api

TTI

Long Task

自定义指标?

Google在RAIL性能评估模型中提出,为了持续吸引⽤户,在 1000 毫秒以内呈现交互内容 同时建议将“⾸屏渲染时间”的终点,视为主⻆元素呈现在屏幕上的时刻

如何定义主⻆元素呈现在屏幕上?

可⻅元素超出屏幕的时刻:自定义指标:统计节点变化

小红书:基于 MutationObserver 观察每⼀次渲染帧 childList,并根据适⽤的算法计算 出 FCP, FMP 等关键渲染帧 timing:

//小红书的指标代码:function(e, t, n) {"use strict";Object.defineProperty(t, "__esModule", {value: !0}),t.observe = function() {if (window.MutationObserver && window.performance && performance.timing) {window.__FP__ = 0,window.__FCP__ = 0,window.__FMP_OBSERVED_POINTS__ = [],window.__FULLY_LOADED__ = 0;var e = void 0,t = 0,n = {},u = void 0,a = void 0; (e = new MutationObserver((function(e) {e.forEach((function(e) {"childList" === e.type ?function e(n) {for (var r = function(r) {var u = n[r];0 === window.__FP__ && u instanceof HTMLBodyElement && (window.__FP__ = Date.now() - performance.timing.navigationStart),0 === window.__FCP__ && (0, i.default)(u) && (window.__FCP__ = Date.now() - performance.timing.navigationStart),u instanceof HTMLElement && (0, o.default)(u) && (_(t, !1),function(e) {function t() {_(e)}u.addEventListener("load", t),u.addEventListener("error", t)} (t++)),u.childNodes && e(u.childNodes)},u = 0; u < n.length; u++) r(u)} (e.addedNodes) : "attributes" === e.type &&function(e) { (0, o.default)(e) && (_(t, !1),function(t) {function n() {_(t)}e.addEventListener("load", n),e.addEventListener("error", n)} (t++))} (e.target)})),function() {if (document.body) {var e = document.body.clientHeight,t = window.innerHeight,n = (0, r.default)(document.body),o = window.__FMP_OBSERVED_POINTS__.length,i = window.__FMP_OBSERVED_POINTS__[o - 1],u = i ? n - i.allElementsNumber: n;window.__FMP_OBSERVED_POINTS__.push({t: Date.now() - performance.timing.navigationStart,layoutSignificance: u / Math.max(1, e / t),allElementsNumber: n})}} ()}))).observe(document, {childList: !0,subtree: !0,attributes: !0,attributeFilter: ["src"]}),function() {var e = XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.send = function() {var n = this;_(t, !1),function(e) {n.addEventListener("readystatechange", (function() {4 === n.readyState && _(e)}))} (t++);for (var o = arguments.length,r = Array(o), i = 0; i < o; i++) r[i] = arguments[i];return e.apply(this, r)}} ()}function _(t) {var o = !(arguments.length > 1 && void 0 !== arguments[1]) || arguments[1];if (0 === window.__FULLY_LOADED__) if (o) {n[t].end = Date.now();var r = Object.keys(n).some((function(e) {return ! n[e].end}));r || (a = t, u = setTimeout((function() {e.disconnect();var t = n[a].end;window.__FULLY_LOADED__ = t - performance.timing.navigationStart,window.__FMP_OBSERVED_POINTS__ = window.__FMP_OBSERVED_POINTS__.filter((function(e) {return e.t <= window.__FULLY_LOADED__}));var o = new CustomEvent("__fullyloaded__", {detail: {firstPaint: window.__FP__,firstContentfulPaint: window.__FCP__,fullyLoaded: window.__FULLY_LOADED__,observedPoints: window.__FMP_OBSERVED_POINTS__}});window.dispatchEvent(o)}), 2e3))} else {var i = Date.now();n[t] = {start: i};var _ = u && i < n[a].end + 2e3;_ && clearTimeout(u)}}};var o = u(n(2)),r = u(n(3)),i = u(n(4));function u(e) {return e && e.__esModule ? e: {default:e}}},

确定标准(tp99?):

前端:秒开

后端:200ms内

分析问题

调试分析:devtools: net、performance、lighthouse;wireshark、burpsuite、Charles、WebPageTest

埋点、监控预警apm、全链路跟踪

录制回放rrweb

devtools:

排队:

资源数量和优先级

chrome 6个tcp并发限制

IO block

SSL:

v1.2->v1.3

秘钥协商:ECDH

数据传输:AES-GCM(AES-NI) 、chacha20(软解) 算法

TTFB:

req size精简:精简header, h2-静态表;

网络链路:cdn、骨干网

服务器处理耗时:各种优化;

下载内容:

响应数据慢:精简、压缩

减少RTT,h2的push

交互优化:

减少JavaScript执行时间;

避免强制同步布局

避免布局抖动

css比js更好

gc导致卡顿

优化

分场景:

加载优化(第一次+第n次)

交互优化

思路:

分类分级

提前-延后-并行、删减

业务和产品层面解决:没有代码就是最快的优化

技术层面解决:浏览器、DNS、IP、CDN、网关、资源服务、API服务

资源分类:不只看页面加载,本质上是看用户体验,减少低端局用户流失比率;

html、js、css;

font(裁剪)、image(@media适配,多分辨率,压缩、webp)、audio/video(压缩、分片、转码、清晰度) 

资源分级:

编译期优化:tree shaking:移除dead code,抽取公共代码,按需加载打包,懒加载,冗余依赖版本统一;

提前:cdn(tcp mss initwnd : 10 -> 70;rwnd)、prefetch、preload、cache、BGP-IP(anycast);预加载

事中:减少: js拆包、压缩:coverage-unused、minify、uglify、gzip/br;并行:雪碧图、内联inline、外部引用按需加载、多域名、h2;直接优化: SSL/TLS 算法优化,密码强度、握手次数...

延后:async、defer、setTimeOut、懒加载

关注:CPU: javascript执行速度(LongTask) - runloop耗时优化-火焰图(v8 - js CodeCache、序列化保存磁盘),GPU:UI复杂度-渲染复杂耗时、layout-shift;内存:系统卡顿(OS mem shrink、 lmk)、(浏览器层:net+io+disk)、兼容性导致卡顿

系统级:cpu调度:进程:静态-动态-优先级,cpu绑定、cpu调度算法;net:tcp->quic(h2-h3)、keepalive、header+body 压缩、滑动窗口初始大小配置、socket buffer size:  wmem、 rmem、、超时时间、tcp拥塞控制算法:cubic、BBR

API分级:

提前:预请求

事中:接口拆分、接口聚合、后台微服务优化:AKF

延后:

RPC协议:  restful:json、grpc:protobuf

设备分级:android设备碎片化,高端机+低端机性能差异巨大- lite轻量版

fake方案:骨架屏、渐进式加载、懒加载、分布渲染、依赖渲染(vue批量渲染导致时序错乱)

服务端优化:

static:nginx disk cache

dynamic:SSR( 对比:SSR : 1700ms , CSR: 2500ms - js-evaluate 耗时严重)

持续保持

事前:版本迭代-退化测试 - 定指标

事后:APM监控,A/B测试

数据量小:全量

数据量大:按用户抽样,按接口抽样、特殊用户用户(boss)-全量

配置开关很重要

Pxx 百分位统计、不是平均值;

CRED:cache、request、error、duration + network(3G、4G、WIFI...)

质量数据、业务数据、反馈数据、舆情数据

埋点-js-sdk -> flink -> Prometheus ;ELK