WebView 与 H5 加速

加载WebView内核

  • 设置缓存
    • webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
    • webSettings.setDatabaseEnabled(true);
    • webSettings.setDomStorageEnabled(true);
    • webSettings.setAppCacheEnabled(true);
  • 在Android 4.2到Android N之间,系统为了组件切换的流程性考虑,setDrawDuringWindowsAnimating为false,反射成true
    • android n以上 & android 4.1以下不存在此问题,无须处理
    • 4.2不存在setDrawDuringWindowsAnimating,需要特殊处理。可以反射handleDispatchDoneAnimating来解决
    • 4.3及以上,反射setDrawDuringWindowsAnimating来实现动画过程中渲染
  • Chrome Custom Tabs
    • 允许 App 直接调用 Chrome 渲染网页
    • 需要用户有安装 chrome,并且被设置为默认浏览器;系统的要求是 4.3(API 18)以上,Chrome 版本 45+
  • 可以在 APP 启动时预先初始化一个 webview 然后释放,这样等用户真正走到 H5 模块去加载 webview时就变快了。
  • 全局WebView(约10M左右的内存占用)
    • 在客户端刚启动时,就初始化一个全局的WebView待用,并隐藏;当用户访问了WebView时,直接使用这个WebView加载对应网页,并展示。
    • 页面间跳转需要清空上一个页面的痕迹,更容易内存泄露,在 APP 运行期间都无法释放。
  • 客户端代理数据请求
    • 在客户端初始化WebView的同时,直接由native开始网络请求数据;当页面初始化完成后,向native获取其代理请求的数据。
  • Webview在Android 4.0上已经默认开启硬件加速,硬件加速如果导致页面渲染问题,可以关闭硬件加速。
  • 内核渲染优化
    • 三个线程(IOThread、MainThread、ParserThread)
    • IOThread会从网络端或者本地获取html数据,并把数据交给MainThread(渲染线程,十分繁忙,用于JS执行,页面布局等),为了保证MainThread不被阻塞,需要额外起一个后台线程(ParserThread)用来做html的解析工作。ParserThread每解析到落地页html中带有特殊class标记的一个div标签或者P标签(图中的first、second)时,就会触发一次MainThread的layout工作,并把layout后得到的高度与屏幕高度进行对比,如果当前layout高度已经大于屏幕高度,认为首屏内容已经完成布局,可以触发渲染上屏逻辑

DNS

  • DNS采用和客户端API相同的域名,可以直接使用缓存的DNS而不用再发起请求图片。静态资源同理。
  • 通过DNS的负载均衡器(一般在路由器上根据路由的负载重定向)可以把用户的访问均匀地分散在多个Web服务器上。
  • 跨域请求移除非必要自定义header,避免该请求发出预检请求。
  • 预读取DNS
    • 新增 dns-prefetch标签
    • 用 meta 信息来告知浏览器,当前页面要做 DNS 预解析
    • 在页面 header 中使用 link 标签来强制对 DNS 预解析

CDN

  • 通过 CDN 向用户分发传输相关库的静态资源文件(常见的图片、JS、CSS 等)
  • 多域名备份。服务端会下发多个 CDN 链接,当用户访问当前 CDN 节点的出异常时,可以快速自动切换到下个 CDN 节点。
  • 快速超时策略
  • 通过配置hosts文件来加快速度;或是自己在内存中管理对应表,在程序启动时查好,而不要在运行时每次都查。
  • 在多线程下面,gethostbyname会一个更严重的问题,就是如果有一个线程的gethostbyname发生阻塞,其它线程都会在gethostbyname处发生阻塞,这个比较变态,要小心。可以试试GNU的gethostbyname_r(),这个的性能要好一些

网络请求

  • 不用域名,用 IP 直连:当然为了安全和扩展考虑,这个 IP 可能是一个动态更新的 IP 列表,并在 IP 不可用情况下通过域名访问。
  • 连接复用,开启 keep-alive
  • minify / gzip 压缩
    • 对于 POST 请求,Body 可以做 Gzip 压缩,如日志。
    • 对请求头进行压缩。这个 Http 1.1 不支持,SPDY 及 Http 2.0 支持。
  • HTTP 协议缓存静态文件HTML 和 JS/CSS/image
    • 询问是否有更新:根据 If-Modified-Since / ETag 等协议向后端请求询问是否有更新,没有更新返回304,浏览器使用本地缓存。
    • 直接使用本地缓存:根据协议里的 Cache-Control / Expires 字段去确定多长时间内可以不去发请求询问更新,直接使用本地缓存。
  • 同步渲染时如果后端请求时间过长,可以考虑采用chunk编码,将数据放在最后,并优先将静态内容flush。可以让后端的API请求和前端的资源加载同时进行。
    • 在HTTP协议中,我们可以在header中设置 transfer-encoding:chunked 使得页面可以分块输出。
    • 让head部分都是确定的静态资源版本相关内容,而body部分是业务数据相关内容,那么我们可以在用户请求的时候,首先将Web API可以确定的部分先输出给浏览器,然后等API完全获取后,再将API数据传输给浏览器。
  • 增量更新。需要数据更新时,可考虑增量更新。如常见的服务端进行 bsdiff,客户端进行 bspatch。
  • 大文件下载。支持断点续传,并缓存 Http Resonse 的 ETag 标识,下次请求时带上,从而确定是否数据改变过,未改变则直接返回 304。

页面框架渲染(DOM下载、DOM解析、CSS请求+下载、CSS解析、渲染、绘制、合成)

  • 减少网页大小,增加带宽
  • 静态化一些不常变的页面和数据,并gzip一下。
    • 一个变态的方法是把这些静态页面放在/dev/shm下,这个目录就是内存,直接从内存中把文件读出来返回,这样可以减少昂贵的磁盘I/O。
    • 使用nginx的sendfile功能可以让这些静态文件直接在内核心态交换,可以极大增加性能。
  • 离线包
    • 将 JS 和 CSS 还有一些图片都内联到一个文件中,这样就只需要一次 IO 操作。用css分块展示。
  • 高性能要求页面还是需要后端渲染(smarty)。相关推荐、广告等可以在内核渲染完后,执行JS,并利用preact进行异步渲染。
  • 首屏静态资源预取缓存
    • 资源内容的变化就一定会引起资源路径变化
    • 给每个资源文件一个版本号或hash值,若资源文件有更新,版本号和 hash 值变化,这个资源请求的 URL 就变化了,同时对应的 HTML 页面更新,变成请求新的资源URL
  • CSS不会阻止页面显示,内联的JS很快执行完成。当这两部分同时出现的时候,CSS加载阻塞下面的内联JS,而被阻塞的内联JS则阻塞HTML的解析。如果必须要在头部增加内联脚本,一定要放在CSS标签之前。
  • 减少重排操作
    • 增加或删除 DOM 节点;
    • display:none(重排并重绘);visibility:hidden(重绘);
    • 移动页面中的元素;
    • 改变元素尺寸(宽、高、内外边距、边框等);
    • 用户改变窗口大小,滚动页面等;
    • 页面初始渲染;
    • 改变元素内容(文本或图片等)。

JS解析、编译、执行

  • JS代码的编译和执行会有缓存,同App中网页尽量统一框架。
  • 把链接数减到最低。
  • 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
  • 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。
  • 第一次查询走数据库获得数据,并把数据放到缓存,后面的查询统统直接访问高速缓存。为每个查询做Hash,使用NoSQL的技术可以完成这个优化。(这个技术也可以用做静态页面)

图片

  • WebP 格式
  • 会将图片和视频等非文字内容通过原生组件的方式放在客户端进行渲染
    • 借由内核的shouldInterceptRequest回调,拦截落地页图片请求,由客户端调用图片下载框架进行下载,并以管道方式填充到内核的WebResourceResponse中

后端

  • 把一堆基本相同的请求批量处理。设置上两个阀值,一个是作业量,另一个是timeout,只要有一个条件满足,就会开始提交处理。
  • 负载均衡
  • throttle(节流阀)。使用throttle技术一般来说是对于一些自己无法控制的系统,比如,和你网站对接的银行系统。
  • 数据分区
    • 把数据把某种逻辑来分类。拆成多张有一样的字段但是不同种类的表,存在不同的机器上以达到分担负载的目的。
    • 把数据按字段分,也就是竖着分表。减少表的字段个数,同样可以提升一定的性能。字段多会造成一条记录的存储会被放到不同的页表里,这对于读写性能都有问题。但这样一来会有很多复杂的控制。
    • 平均分配,通过主键ID的范围来分表。
    • 同一数据分区。把同一商品的库存值分到不同的服务器上。
  • 数据镜像。可以做负载均衡,还可以有高可用性。
  • 数据冗余处理,减少表连接这样的开销比较大的操作,会牺牲数据的一致性。需要根据不同的业务进行分析和处理。把NoSQL用做数据,对数据一致性有大的风险。