整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

前端网页加载渲染链路优化

面性能和用户体验的各个指标怎么来优化呢?open signal官方提供了2018年2月份统计的全世界4G网络覆盖率和通信速率的统计分布图如下,在目前移动互联网的浪潮下,我们要利用好用户终端设备的每个字节的流量。

当然页面性能和体验优化并不是一蹴而就的,需要不断的研究、跟踪,发现问题,解决问题。但是我们可以在一开始编写业务代码的时候就做的更好,做到极致。所以,关于优化实战我们主要分为两部分:加载渲染链路优化 和 编程代码优化。

加载渲染链路优化

从访问url到页面呈现,整个链路可以做优化的思路。

幸运的是,W3C推荐的Navigation Timing标准中所定义的核心的页面性能数据,它包含了从上个页面销毁到跳转到当前页面加载完成每个阶段所消耗的时间。在canIuse上查到的兼容性也很好:

利用这个接口可以很方便的帮助我们排查链路问题。在Navigation Timing标准中介绍到这个API主要包含两个接口:PerformanceTiming和PerformanceNavigation,这两个接口由浏览器进行实现和维护,当浏览器创建页面的时候就会把接口定义的相关数据挂载到window.performance.timing和window.performance.navigation这两个属性上。我们可以打开一个网页看一下:

我们把这两个图对比一下,就可以很容易的排查出页面的加载链路问题。

静态资源链路

打开页面的第一步是请求页面的html,这里面涉及TTFB这个综合指标。同时如果有必要我们也可以统计DNS时间和TCP时间。

DNS时间:主要是根据请求域名查询到对应主机IP的时间。这个和DNS服务器有关系,也可能和本地缓存有关,如果这个很慢,可以找服务商排查下问题。

TCP时间:tcp是承接http协议的下层协议。主要是路由到主机ip,并建立tcp链接的时间。这个时间反应了服务器到用户客户端之间链路是否通畅,网络是否通畅。

请求完HTML之后,就开始解析html代码,按照从上至下、自然顺序解析,解析内联CSS代码或者加载外链CSS脚本,解析内联Javascript脚本,或者加载外链Javascript脚本。由于浏览器是单线程的,这些CSS和Javascript脚本很可能就会造成页面卡顿。参考 浏览器线程理解与microtask与macrotask。

加载

CDN是内容分发网络,主要用于缓存静态资源。CDN服务商一般会在全国各地部署服务,而且带宽很大,这样访问CDN的资源时就可以有较短的路由路径,而且带宽也比较大,访问比较快。

  1. 建议最好把html, CSS、JS、font、img这些资源放在CDN上,没有CDN也可以放在OSS存储服务上,总之比自己的服务器硬盘快多了,至少服务商会在不同区域做分布式部署
  2. 如果没有钱买CDN服务,那么就尽可能少的加载外联CSS和JS代码,注意html头部可以增加dns-prefetch,减少DNS解析时间
  3. 不是在首屏展示的资源,不要立即加载,可以在页面onload之后加载,或者首屏渲染完成再加载
  4. 压缩CSS、JS、font、img,尽量减少体积,服务端开启gzip
  5. 考虑资源combo请求,减少http请求量,浏览器一般都有并发限制, 比如chrome一次6个并发http请求,不同浏览器内核可能不一样。
  6. <script>加载脚本会阻塞浏览器主线程,考虑异步化,参考 script标签的defer与async
  7. 利用好缓存,利用好http响应头缓存字段,开启静态资源缓存,减少资源下载,建议开启service worker缓存,这个是作为APP Cache的替代方案,参考MDN;
  8. 对于单纯的获取数据做展示,尽量采用JSONP请求数据,而不是AJAX,提升数据请求性能。参考jsonp而不是AJAX?
  9. 开启HTTP/2 ,HTTP2支持链接复用,可以很高效下载多个小文件。HTTP/2 的目的是通过支持完整的请求与响应复用来减少延迟,通过有效压缩 HTTP 标头字段将协议开销降至最低,同时增加对请求优先级和服务器推送的支持。

解析渲染

加载完JS和CSS之后,浏览器开始解析执行。Chrome的渲染流程是这样的:(可以参考 高性能CSS动画)

为了让浏览器更快的解析渲染,我们需要考虑这几点:

  1. CSS嵌套层级不要太深,不超过3级,避免在最内层使用通配选择器。参考关于 CSS 选择器性能
  2. JS脚本不要太复杂,考虑轻量化架构,降低JS复杂性,减少解析时间,尽量不要引用复杂的第三方脚本。
  3. 按需加载模块,按需打包,首页仅仅加载和执行和首屏相关的脚本。其他脚本延迟加载执行。
  4. 考虑依赖的第三方模块是不是必须,需不需要精简。
  5. 打包优化,code split 和 tree shaken。常用webpack和rollup的优化。
  6. 用户交互相关事件绑定(比如页面scroll,用户左右滑动等),添加参数{passive:true},减少浏览器事件等待。因为这些事件属于可阻止事件,浏览器不知道用户会不会阻止,所以需要等待js执行,然后再做响应。添加passive参数,就告诉浏览器不用等待了。
  7. IOS8以后的ios支持wkwebview,但是很多app之前用的还是uiwebview,建议转换成wkwebview,获得性能的提升(UIwebview在执行JS时会阻塞UI渲染进程,WKwebview不会)。

介绍一下code split的方案: react-loadable

// 未处理
import OtherComponent from './OtherComponent';
const MyComponent = () => (
 <OtherComponent/>
);
// 使用react-loadable按需加载
import Loadable from 'react-loadable';
const LoadableOtherComponent = Loadable({
 loader: () => import('./OtherComponent'),
 loading: () => <div>Loading...</div>,
});
const MyComponent = () => (
 <LoadableOtherComponent/>
);

这个也可以在打包工具统一配置,不用每个模块都自己写。

只有浏览器尽快渲染出来,用户才能尽快的可以交互。

数据埋点

上面我们梳理了加载到解析渲染过程应该做的事情,那么如果你这些都做好了,发现网页表现依然不尽人意,那么你就要考虑做一下数据埋点。其实数据埋点在企业项目中也是必不可少的,和性能体验优化构成闭环。通过数据来发现页面性能和体验的问题,更有针对的进行解决。

事实上数据埋点分为三类:

  1. 业务埋点,统计诸如pv、uv、点击率、流失率、转化率等
  2. 大数据埋点,统计与用户行为相关信息,比如那个用户点击了那个商品,上报用户id和商品id,方便后台分析用户和商品的关系,可以用做大数据分析,推荐算法来为用户推荐商品。

工程埋点,统计工程上的数据信息,比如页面秒开率,dns时间等,也就是我们上节课总结的性能和体验数据指标。

资源缓存

这一节我们单独介绍缓存,是的,利用好缓存可以解决很多问题,包括页面加载和渲染的问题都能得到很好的优化。

常见的h5缓存方案有很多种,

通常,与页面加载性能相关的,有下面几种缓存,

(1)MemoryCache

MemoryCache,资源存放在内存中,一般资源响应回来就会放进去,页面关闭就会释放。内存存取性能可达磁盘缓存性能的100倍,但这还不是MemoryCache的最大优势,MemoryCache最大的优势是离排版渲染引擎非常近,可以直接被读取,甚至无需经过线程转换。在真实的页面访问过程中,获取资源的时间,磁盘IO仅仅是其中的一部分,更多的时间往往消耗在各种线程抛转。

(2)ClientCache

ClientCache,客户端缓存,比如,手淘里的ZCache(离线压缩包缓存),本质上属于磁盘缓存。这类Cache的优点是能以相对可控的方式让资源提前缓存在磁盘,但它也有一系列的成本。比如,它需要一套服务器与客户端协同的下发更新逻辑,服务器端需要管理下发,客户端需要提前解压缩。我们可能觉得提前解压并不是什么弱点,但如果有一千个离线包,这个问题就比较严重了,如果不提前解压,就无法保证首次访问性能,如果提前解压会让IO非常繁忙,可能会造成客户端打开时严重卡顿。

(3)HttpCache

HttpCache,是历史比较悠久的缓存,它利用标准的 Cache-Control 与服务器端进行协商,根据标准的规则去缓存或更新资源。它应用非常广泛,是非常有效果的一种磁盘缓存。它的缺点是完全由浏览器按标准规则控制,其它端的控制力度非常弱。比如,某些被HttpCache缓存的静态资源出问题了,通常只能是改页面,不再使用出问题的资源,而无法主动清除出问题的资源。参考http请求缓存头,HTTP协商缓存VS强缓存原理

(4)NetCache

网络相关的Cache,一般是指DNS解析结果的缓存,或预连接的缓存。DNS预解析和预连接是非常重要的,创建一个Https连接的成本非常大,通常需要600ms以上,也就是说,页面如果有关键资源需要全新建连接,秒开基本是不可能了。

(5)CDN

CDN一般是通过负载均衡设备根据用户IP地址,以及用户请求的URL,选择一台离用户比较近,缓存了用户所需的资源,有较好的服务能力的服务器,让用户从该服务器去请求内容。它能让各个用户的缓存共享,缩短用户获取资源的路径,来提升整体的性能。

当然,还有其它非常多类型的Cache,比如,

JS相关,V8 Bytecode Cache,字节码缓存,能极大的减少JS解析耗时,甚至可以提升3-6倍的性能。参考:前端优化系列 – JS解析性能分析

渲染相关,图片解码数据缓存,是一块非常大的内存缓存,约100M,能保证页面滚动过程可以实时获取到图片解码数据,让滚动非常流畅。

页面相关,页面缓存,Safari的PageCache,Firefox的Back-Forward Cache,UC浏览器的WebViewCache,都是一样性质的缓存,将整个执行过的页面保存在内存。标准的页面缓存,进入的条件非常苛刻,大部分情况都无法进入,而且在前进后退的场景才允许使用。

缓存优化实例

前面介绍了很多理论层面的内容,我们接下来介绍一些实践优化案例。

(1)预置资源进MemoryCache

在页面的onPageFinished的回调里面去检查是否有资源可以预置,如果有,就通过相关接口把资源设置进内核的MemoryCache。我们并不知道用户即将会访问什么页面,如果把大量的资源都预置进内存,而用户却没有使用,那就会造成浪费。另外,资源在内核内存,仅仅是加快了资源的加载速度,页面的首屏包含非常多非常复杂的流程,某个流程的加速并不一定能带来整体性能的提升,比如,非关键的JS放在内存,可能就会先于一些关键JS被提前执行,反而让首屏更慢。所以,选择放那些资源进内存也是非常有讲究的,能预置的资源一般是 非常关键的更新频率较低的少量公共基础资源。

对于一般公司来说,没有能力自己定制webview渲染的内核,可以看下系统默认webview内核有没有这样的接口来实现操作MemoryCache预置数据的能力。

(2)预加载资源进HttpCache

预置资源进内存,对加载性能的提升是最明显的,但成本也是最大的,会占用用户手机宝贵的内存资源。另外一种预置资源的思路是,提前通过内核去预加载一些资源,资源加载回来之后就直接保存在标准的HttpCache。资源在HttpCache和在客户端缓存(比如,手淘ZCache)的性能差别不大。但如果资源不能放进ZCache,通过这种方式提前放到HttpCache,也是一种优化思路。

(3)使用WebViewCache极速切换页面

H5页面的加载流程是非常重的一套流程,即使同一个页面多次重复访问,也需要走比较完整的流程,耗时极长,这与用户的期望是不符的,通常用户期望访问过的页面就能快速展现出来。在一些特定的场景,H5也是可以做到极速展现的,比如,前进后退。其它的场景,比如页内几个TAB切换,是否也可以用上这类缓存呢?也是可以的。原理上也是比较简单的,在页面首次访问时,会将排版渲染好的页面放进WebViewCache里,WebViewCache是存储完整页面的一块内存。

用户再次访问该页面时,会将WebViewCache内存中的完整页面读取出来,直接绘制展现,而无需再进行加载解析排版渲染等流程,从而达到极速打开的效果。

除了内核提供WebViewCache基础技术之外,前端也需要与内核进行一定的交互,比如,通过JSAPI查询当前页面是否在WebViewCache,如果在则返回它在WebViewCache列表的位置,然后前端就可以使用JSAPI去跳转到相应位置的页面,内核就把页面从内存读取和展现出来。使用此类技术,页面一般能在500ms左右完全展现出来,具有非常好的用户体验。

当然这个也是需要浏览器内核提供这种能力,如果公司有自己的内核开发团队,可以做到定制。

(4)前端使用LocalStorage缓存HTML文档

当前前端渲染非常流行,页面大部分的逻辑都会由前端JS去执行,JS执行完才会生成完整的HTML文档,而JS执行的成本是非常大的,JS执行时间可能占据首屏时间的50%,有些甚至能达到80%。那么,我们有没有可能将JS执行生成的完整HTML文档缓存起来呢,下次访问时直接使用已缓存的页面,而无需重复执行JS?这也是可以的原理上也不复杂,首次访问页面时,JS执行完之后会生成完整的HTML文档,我们将HTML文档缓存到LocalStorage里面。

在后续的访问中,我们优先从LocalStorage里面读取HTML文档,解析排版渲染页面,而无需JS执行去生成页面,让页面展现速度得到极大的提升。

这种方案的关键在于前端能够实现一套DOM-Diff更新的机制,在从LocalStorage读取HTML页面的同时,前端还会发起请求去更新HTML文档,在新的HTML文档回来之后,会和旧的文档进行Diff,针对Diff来进行局部更新,这样能保证页面得到及时的更新。

(5) service worker

参考使用 Service Workers提升体验,这里附带介绍下这个方案,目前service worker 只有在android的webview中可用,ios还不支持。我们通过先注册一个serviceworker服务,指定哪些资源和数据需要存储,然后下次请求页面会自动激活这个service worker,页面请求时会先从service worker中返回缓存的数据。当然service worker中需要自己处理版本和维护数据更新。

、介绍

在docker中可以将容器中的目录挂载出来,在k8s中pod可以部署在不同节点,假如该节点的机器宕机了,k8s可能就会将此Pod转移到其他机器,就不是原先的机器了。k8s有自己的一套挂载方案,如下图所示,

原理为将所有节点的挂载的目录统一抽象管理为叫做 存储层的概念,使用技术如NFS网络文件系统,在每一个节点都会有如50G的存储空间,该空间之间的数据是同步的,这样就解决了某一节点宕机了,Pod转移到其他节点了依然可以获取到挂载的目录。



1、搭建NFS网络文件系统



  • 所有机器安装 yum install -y nfs-utils
  • nfs主节点,这里我们对应k8s的主节点(我配置两个主节点,选择了其中一个) 创建同步文件的目录 mkdir -p /nfs/k8s/data,设置暴露目录echo "/nfs/k8s/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports
  • nfs主节点启动RPC远程绑定现在启动并且开机也要启动 systemctl enable rpcbind --now
  • nfs主节点启动nfs 服务端,并且开启自动启动 systemctl enable nfs-server --now
  • nfs主节点使配置生效 exportfs -r, 输出目录查看 exportfs



  • 其他节点可检查哪些目录可以同步,执行命令 showmount -e 192.168.134.110,注意IP换成自己的



  • 其他节点也创建同步的目录,目录名随意,我定义成一样的 mkdir -p /nfs/k8s/data
  • 将主节点和其他节点关联起来 ,在其他节点执行 mount -t nfs 192.168.134.110:/nfs/k8s/data /nfs/k8s/data
  • 测试,可以在任意一个节点添加修改文件,去其他检查是否同步修改了

2、原生方式创建Deployment并应用目录挂载到nfs

如下使用yaml的方式将镜像nginx中的目录/usr/share/nginx/html挂载到了nfs服务端的/nfs/data/nginx-pv目录,只要在挂载目录/nfs/data/nginx-pv里加文件,也会同步到了pod里/usr/share/nginx/html

kubectl apply -f nfs-demo.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-pv-demo
  name: nginx-pv-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-pv-demo
  template:
    metadata:
      labels:
        app: nginx-pv-demo
    spec:
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
        - name: html
          nfs:
            server: 192.168.134.110
            path: /nfs/data/nginx-pv

注意挂载的目录一定要先创建好否则查询当前应用时出现如下一直在创建中



进入这个pod详细信息,可以看到报错的详细问题 kubectl describe pod nginx-pv-demo-dc6884649-4phvz



这种方式缺点:1、需要提前创建好挂载的机器目录;2、当Pod删除,机器挂载的目录不会同时删除;3、无法指定目录的存储大小

3、使用静态PV/PVC持久卷方式

PV持久卷,可以指定大小,当PVC删除,并释放对应的PV



PV:持久卷(Persistent Volume),将应用需要持久化的数据保存到指定位置

PVC:持久卷申明(Persistent Volume Claim),申明需要使用的持久卷规格

2、存储挂载PV/PVC实战

1、在nfs目录中创建PV的静态目录

#nfs主节点
mkdir -p /nfs/k8s/pv/data/01
mkdir -p /nfs/k8s/pv/data/02
mkdir -p /nfs/k8s/pv/data/03

2、创建静态PV持久卷

执行命令 kubectl apply -f pvdemo.yaml

以下可以写在一个文件中 ,用---分割,会自动识别成三个文件,server为主节点的IP地址。storageClassName表示一个标志,在后面创建PVC时需要用到。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv01-10m
spec:
  capacity:
    storage: 10M
  accessModes:
    - ReadWriteMany
  storageClassName: nfs
  nfs:
    path: /nfs/k8s/pv/data/01
    server: 192.168.134.110
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv02-1gi
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  storageClassName: nfs
  nfs:
    path: /nfs/k8s/pv/data/02
    server: 192.168.134.110
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv03-3gi
spec:
  capacity:
    storage: 3Gi
  accessModes:
    - ReadWriteMany
  storageClassName: nfs
  nfs:
    path: /nfs/k8s/pv/data/03
    server: 192.168.134.110

3、查询PV持久卷


简写


4、创建PVC

pvc.yaml如下:storageClassName: nfs 对应创建的PV

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: nginx-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 200Mi
  storageClassName: nfs

因为申请的storage: 200Mi 指定了大小,所以被分配了拥有1G容量的PV



5、删除PVC

对应的PV也会被释放Released



6、查询PVC



7、创建Deployment应用同时挂载PVC

执行 kubectl apply -f pvcdemo.yaml

将创建好的PVC nginx-pvc挂载到当前创建的应用中。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-deploy-pvc
  name: nginx-deploy-pvc
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-deploy-pvc
  template:
    metadata:
      labels:
        app: nginx-deploy-pvc
    spec:
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
        - name: html
          persistentVolumeClaim:
            claimName: nginx-pvc

8、测试

修改挂载的目录,即可查看文件有没有同步。

告:

HostPath 卷存在许多安全风险,最佳做法是尽可能避免使用 HostPath。 当必须使用 HostPath 卷时,它的范围应仅限于所需的文件或目录,并以只读方式挂载。

如果通过 AdmissionPolicy 限制 HostPath 对特定目录的访问,则必须要求 volumeMounts 使用 readOnly 挂载以使策略生效。

hostPath 卷能将主机节点文件系统上的文件或目录挂载到你的 Pod 中。 虽然这不是大多数 Pod 需要的,但是它为一些应用程序提供了强大的逃生舱。

例如,hostPath 的一些用法有:

  • 运行一个需要访问 Docker 内部机制的容器;可使用 hostPath 挂载 /var/lib/docker 路径。
  • 在容器中运行 cAdvisor 时,以 hostPath 方式挂载 /sys
  • 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。

除了必需的 path 属性之外,你可以选择性地为 hostPath 卷指定 type。hostPath卷类型支持的type值如下:

取值

行为


空字符串(默认),用于向后兼容,意味着在安装hostPath卷之前不会执行任何检查

DirectoryOrCreate

如果在给定路径上什么都不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 kubelet 相同的组和属主信息。

Directory

在给定路径上必须存在的目录。

FileOrCreate

如果在给定路径上什么都不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 kubelet 相同的组和所有权。

File

在给定路径上必须存在的文件。

Socket

在给定路径上必须存在的 UNIX 套接字。

CharDevice

在给定路径上必须存在的字符设备。

BlockDevice

在给定路径上必须存在的块设备。

当使用这种类型的卷时要小心,因为:

  • HostPath 卷可能会暴露特权系统凭据(例如 Kubelet)或特权 API(例如容器运行时套接字),可用于容器逃逸或攻击集群的其他部分。
  • 具有相同配置(例如基于同一 PodTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为。
  • 下层主机上创建的文件或目录只能由 root 用户写入。 你需要在特权容器中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

hostPath卷类型测试

测试选择在第二个节点服务器上使用hostPath,将Pod调度限制在该节点,如果不加以限制,Pod可能会被调度到其他节点服务器上,Pod无法启动。

// hostPath 类型卷配置文件
[root@c7-1 hostpath-manual-storageclass]# cat pv-volume-hostpath.yaml 
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath10g
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"
// pvc 申领配置文件
[root@c7-1 hostpath-manual-storageclass]# cat pvc-hostpath-manual.yaml 
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-hostpath10g
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
// Pod 配置文件,引用hostPath类型的卷
[root@c7-1 hostpath-manual-storageclass]# cat pod-hostpath.yaml 
apiVersion: v1
kind: Pod
metadata:
  name: pod-pv-hostpath
spec:
  nodeName: c7-2.runoqd.com		// 指定该Pod被调度到的节点
  volumes:
    - name: pv-storage10g
      persistentVolumeClaim:
        claimName: pvc-hostpath10g
  containers:
    - name: pod-pv-hostpath
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: pv-storage10g

创建相关资源并验证