这一篇想讲点老本行。寒假的时候做了一个网页,类似于相册。用大家熟悉的词说,有点“H5”的感觉(然而并不想用这个词)。在这个项目里,我很多精力都被用在优化用户体验上了,觉得应该分享一下。

网页性能的优化

页面用到了 canvas。这部分我是在别人的代码的基础上做了很大的改造再完成的。

跟图形处理一相关,网页的运算性能就会变得很重要,尤其是每一次交互操作都需要进行绘图、需要在手机上运行的时候,要是画面跟不上交互,看着就特别难受。在把基本功能都实现了之后,自然需要处理这个问题。

首先,交互事件(尤其是触摸事件)触发次数非常多,每次事件都触发绘图,性能自然会特别不好。幸而现代浏览器都会提供 requestAnimationFrame(),利用这个接口,再对事件触发后的操作进行“缓存”,多次触发只会执行一次绘图,每次绘图多画几个点,绘图的频次就会下降很多,而且跟浏览器同步绘图,看着流畅。

当时也纠结过触摸事件的触发频率,为了避免画面出现难看的直线夹角,甚至还去查了 N 次曲线的插值,运算速度肯定会比较慢。后来看了下实际的触发,还是很频繁的,点与点之间直接拉直线画过去就行,效果还可以,性能自然比较好。

通过 Chrome 开发者工具里的 Profile,可以看到每个 JavaScript 函数的运行时间。最后发现几个耗时比较长的动作,也了解了 canvas 的硬件加速机制:

  • canvas 下的 ctx.getImageData() 耗时最长,基本上要七八十 ms,还是同步的。据说这个过程需要从 GPU 里读数据,因为 GPU 的特性,速度奇慢。而程序需要用这个接口读取画布情况,来触发事件。自己写一个虚拟画布太费功夫,所以最终还是只得通过按需调用、延时调用的方式,减少调用次数,减少对性能的影响。
  • 有些互动,需要在 canvas 里画一个带渐变色的圆,正常是每次都调用一次带渐变色的图形生成函数,直接把图形叠在 canvas 上,因为涉及到渐变色的计算,生成速度比较慢。然而 ctx.drawImage() 的耗时却特别少,估计在 GPU 上叠图是很快的事情。因为这个渐变色的圆不会变大小,最终采用的是程序启动时,先生成这个渐变圆,再 base64 存成 Image 对象,每次要画图的时候直接把这个 Image 对象拿来叠。

优化过之后基本能保证 40 - 50 fps 运行了,因为 getImageData 的原因,60 fps 还是比较麻烦,但肉眼真的没什么差别,就这样子吧。

JS 辅助的自适应排版

写完基本的交互,接下来就该处理界面了,这还是个很烦人的问题。由于界面有图文混排,自适应排版有一定的难度。最早还准备纯 CSS 解决,后来发现还是得 JavaScript 辅助调整,也就是那种看起来很 low 的绑定在 resize 事件上、加了 requestAnimationFrame() 缓冲的、强写宽度的方法。呃现在想起来我似乎忘记了有 display: flex; 这个东西,算了过去了。

用程序重设界面有个好处,特别灵活,毕竟 CSS 是做不到比较两个元素的宽度来调整显示的,即便做到也肯定没有 JS 灵活。

排版这部分的核心代码有 50 多行,除了会根据底部高度的空白剩余情况判断是否调整背景的显示(其实这只占几行),有十多行是来处理横屏状态的。横屏会随着窗口宽度的增大,计算边栏剩余宽度,从“以图为主”变成“图文比例分配宽度”的模式,并且界面从占满宽度变成自动居中。如果没有 JS 辅助,还真的没法这么灵活。

最终的效果,经过 Surface、iPad、iPhone、Android 手机的横竖屏实测,无论是小屏还是超大屏,都会有很出色的排版效果,而在极端情况下尽量保证可用性。自适应设计可是一个网页的自我修养的极致啊。

另外在写这篇东西的草稿的时候,才想起来忘记设计打印机样式了。不过一个互动相册要喂给打印机,还真的挺难弄的。最后实现的是只打印用户创作结果,大概就是原样打印了,不过要在纸上排好版,还是需要 JS 的脏活的。

处理键盘事件

键盘事件的处理其实只要注意到了就好,技术难度基本为 0。基本上,对于每一次 JavaScript 的互动变化,都要注意重设键盘焦点的位置;另外对弹出窗口这类东西,要监听一下 Esc 键,这个是一种美德。总体效果还是特别流畅的,主要操作基本不用碰 Tab 键。(题外话,现在 Win 10 的键盘焦点的交接做得太明显了吧,看着突然觉得特别突兀)

当然还有一个棘手的问题:Android 上的返回键。当然这也没什么难的,去年 bPlanner 的时候我把 jQuery.modelBox 改造好了,用上了 statechange 事件,直接拿来用就是了。

不过最棘手的莫过于微信的 x5 内核根本不鸟 onbeforeunload 事件,看起来这个接口因为被广泛滥用,最终只开放给京东使用。但人家照片看了一半,一不小心手滑就得从头开始,体验的确不好。

虽然作为一个网页,每一个界面都做一个永久 URL 是一种美德,但我并不想这么做,主要是垃圾微信在分享的时候是不支持标准的 canonical 地址的,动态改 URL 的话分享会很怪异。最终呢,只有欢迎页和主流程页面两个 state,如果你在看照片的时候一不小心按到了返回,state 会跳到欢迎页,就会弹框提示是否返回到欢迎页,这部分情况就能被页面程序处理了。退回到欢迎页之后,再按退出就不会受到阻拦了。

最后体验居然还不错,而且成品里我根本就没绑定 onbeforeunload 事件。但现在仔细一想,整个逻辑还是有点怪异,有优化的空间。感觉在这种应用里,似乎也没必要做 onbeforeunload 提示吧。

网页体积一减再减

一个网页的美德,包括别下载没必要的文件,现在很多网页都有这个毛病。其实省流量也是省钱,当图片 CDN 访问量巨大到半天跑掉 2GB 流量的时候,就会心疼了。

经过好几天的折腾,这个网页的成品第一屏 65 KB,最佳状态下总花费流量在 5MB 左右(这可是包含了二三十张“高清大图”呢)。这块我也刻意做了很多工作。

首先,从一开始我就不准备用辅助的程序库,可以省下好几十 KB,当然代价就是老浏览器不用支持了,我用的 jQuery.modalBox 也得解掉对 jQuery 的依赖,全得自己改(不过就几百行而已)。仔细一想,这种页面也没必要支持老浏览器,有些特性 polyfill 下来交互性能反倒不行。写下来感觉用 vanilla JS 还是挺爽的。

写的判断浏览器支持的函数也很重要,有些特性判断是可以“化简”的。最终, canvas 支持和 classList 支持的判断被化简了,如下:

var i = new Image();i = 'crossOrigin' in i;
// via Modernizr: requestAnimationFrame, es5 > canvas, classList
return 'requestAnimationFrame' in window && i && 'matchMedia' in window && !!(Function.prototype && Function.prototype.bind);

在正式发布之前,我集成了 Sentry,结果经过几百次访问之后看到报错,Android 2.3 的浏览器连 matchMedia 也不支持,果断就把这个属性加进兼容检查了。错误自动收集还是很有用的工具呢。

当然还得记住,在程序正式运行之前,要尽量让判断浏览器兼容性的部分不存在兼容性的问题,否则它自己都报错就没意思了。

写完程序和 CSS,扔给 uglifyjs 和 cleancss 压缩也是很自然的啦,顺便还发现 uglifyjs 得扔一个 -m 的参数给它才会压缩变量名。压缩下来包括 HTML,一共 15 KB。至于统计的 ga.js 20KB、背景图死活优化到的 20KB、JSON 数据 4KB、Sentry 的 10KB 我也无能为力了……

然后就是打开开发者工具看网络性能了。一些琐碎的优化如下:

  • 为了微信这个分享时只会读“第一张大于 200px 的图”的 2B,我在页面插了一张隐藏的图,经过测试动态载入的图片也可以,于是我把那个 <img> 的 src 改成了 data-src,JS 里判断到微信之后再动态把属性改回来。微信要求浪费的流量,别的浏览器可不浪费。结果似乎最新版微信客户端还死活不认了,好气。
  • 能不用 <img> 的就不用 <img>,对于极少改动的图片,即便显示看起来是 inline 的也改用 CSS 嵌入。这样在图片所在的 <div> 被隐藏的情况下,是不会下载图片的,然而 <img> 的图片会被提前下载,会增加首屏加载负荷。
  • 服务器最早不会对 JSON 做 Gzip 压缩,浪费了好多 KB 的流量,果断去设置服务器了。
  • 逛 UPYUN 的时候发现他们支持 WebP 实时压缩,果断给 JS 加了判断代码,支持 WebP 就调用 WebP 转换的地址。这可是 1600*1200 的超大 JPG 啊,测试下来显示效果基本没差别,WebP 居然比 JPG 节省了 20% 以上。
  • 上线了几天之后忍不住还是写了基于 UPYUN 的自适应图片,分成了宽度 640px、1080px、1600px 和原图四档,顺便也做了实时远程切图,最后的流量节省效果是很明显的,同样的环境下流量从 8MB 减少到了 5MB。

必须自夸一下动态预加载。在每一组照片显示完成后,下一组照片会开始预加载,这个设计很普遍。但从欢迎页开始的时候,需要拉动一个拉杆到最右侧。实际上在拉杆开始被拉动的时候,就会触发对第一组照片的预加载。这个设计呢,一是尽量节省用户流量,用户不打算进入的时候是不会加载几百 KB 的图片的,对用户意图的判断会比较准确;另外也有助于用户适应页面的互动方法,不刻意在后面给出操作提示,降低学习成本(拉杆操作会在整个页面的交互流程中起到重要作用)。

写完这篇总结,发现优化的确是没有止境的,不过 best practice 总结下来了,下次想到了肯定用得上。你会发现,网页技术随着终端的更新换代和标准的进步,功能越来越强,学问越来越多,做到”好体验“需要花的功夫也更多,真的别小看它了。

最后,当然还是要放个链接。朋友圈里写了句“这算是个小交互设计作品而已,跟校庆可能并没有什么关系”,或许吧。

“分享一些打磨单页网页的经验”的一个回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注