在持续开发一个核心系统过程中,除了满足业务需求外,还应该考虑系统未来的架构,追求极致的系统的可用性、高性能和稳定性。这个过程是一个长期积累和重构的过程。
每个应用都要满足自己特定的需求,因为商业条件、应用场景、用户期望,以及功能复杂性各不相同。尽管如此,如果应用必须对用户作出响应,那我们就必须从用户角度来考虑可感知的处理时间这个常量。事实上,虽然生活节奏越来越快——至少我们感觉如此,但人类的感知和反应时间则一直都没有变过:
时间 | 感觉 |
0~100ms | 很快 |
100~300ms | 有一点点慢 |
300~1000ms | 机械在工作呢 |
>1000ms | 先干点别的吧 |
>10000ms | 不能用了 |
这个表格解释了Web 性能社区总结的经验法则:必须250 ms 内渲染页面,或者至少提供视觉反馈,才能保证用户不走开。如果想让人感觉很快,就必须在几百 ms 内响应用户操作。超过1s,用户的预期流程就会中断,心思就会向其他任务转移,而超过10s,除非你有反馈,否则用户基本上就会终止任务!
下面将从前端、服务器端、缓存、兜底等来说说如何优化京东三级列表页。
前端优化
京东三级列表页从优化到上线,已经经历了两个618和一个双11的考验,每天有上亿的访问量,页面打开时间在20-80毫秒(在某些地区或低带宽下会大于100ms)。
优化四原则
精简和瘦身页面,首屏优先展示出来;
需用户交互的部分惰性加载;
能不执行的先别执行,惰性执行;
滚屏惰性加载。
一、HTML文档要精简
目的:尽快渲染出页面并达到可交互的状态。
方法:
1、如果非必须,尽量只生成首屏需要的html数据;
2、优先获取资源、提前解析。如首屏需要的css和js;如果不考虑维护成本,可以把首屏需要的css和js放到文档中;
3、发现和优先安排关键网络资源,尽早分派请求并取得页面;
4、文档精简后,服务端生成程序耗时短,性能才会好。
如列表页的头、面包屑、品牌区、属性筛选区、60个商品主图数据,这些是服务端模板渲染输出;而剩余部分是在前端JS惰性加载或生成。
二、需用户交互的部分惰性加载
对于三级列表页品牌区,服务端只渲染18个品牌,用户在点更多时,ajax异步加载其他的。对于整个属性是筛选区服务端只渲染5行,其他行用户在点更多时,js从文档嵌入资源中取到数据,并渲染成html。这样做可以保证服务端计算少,提升服务端性能,减少数据传输。如下图点“更多”时才加载更多的品牌,因为有些三级类目有非常多品牌,如果不采用这种方式,整个页面渲染非常慢。
因为需要SEO的原因,京东三级列表页不能使用bigpipe等技术来进行更优的处理。
三、能不执行的先别执行,惰性执行
上图是三级列表页最重要的商品区(商品主图+N个关联商品小图),每个商品的区域都是完全一样的;如果在服务端拼装整个商品区域的话,尤其涉及到小图部分,会有非常多的重复html元素;我们把体验和减少页面内容进行了折中处理;服务端渲染输出商品主图部分;小图部分通过json数据嵌入到页面,然后通过js惰性执行渲染。这样可以很好地对页面进行瘦身。而且小图资源是页面嵌入的,非异步加载;没有网络请求,用户基本感知不到异步带来的渲染闪动问题。下图就是页面嵌入的小图json数据。
四、滚屏惰性加载
三级列表页的60个商品区域的图片和页尾都是当用户向下滚动页面时,才去加载当前屏幕中的图片和模块。这样可以节省服务器带宽和压力,提升页面整体渲染时间。
上边就介绍完了三级列表页在优化时使用的最主要的四个原则,而实际优化过程中,还涉及到非常多的优化细节,如下部分将介绍这些细节。
将一些JS/CSS资源直接嵌入页面
把资源嵌入文档可以减少请求的次数。比如页面需要的js 、css数据。如下图所示:
上图中的这些js对象,是后端渲染输出的,因此不适合放入单独的js文件,直接在页面中嵌入输出会更好些。slaveWareList是小图的列表对象。如果放在服务端模板渲染输出的话,首先需要进行一些循环拼装页面;另外会使页面体积变得非常大。权衡之后决定放到前端js渲染输出。这样也带来了一些好处:减轻服务端压力,提升渲染模板性能和减少服务端执行时间;服务端不用生成html,文档减少上百个div,减少页面大小和网络开销;提前放到文档中,不用异步调用;用户基本感知不到渲染过程。
对引入的资源排定优先次序
根据自己系统的业务,对每种资源定优先级:对必需的资源优先加载,而低优先级的请求保存在队列中延时加载或等待必需资源加载完再加载;如:搜索推荐热词、顶部三个热卖商品接口、60个主商品的图片、价格优先加载。而对于库存、促销信息、广告词、预售商品、店铺信息等,延后加载。对于点击流,广告统计数据则延时两秒再加载。
应用js缓存来存储公有属性和商品信息属性
三级列表页中的每个商品都是一个对象,存放在一个map中,通过ajax接口异步填充和维护商品的属性。用于后续用户交互用。同时维护成本也会降低;即页面中用到的每个商品数据放入一个map中,如果没有则异步加载;如果有直接使用;即这些数据是公共数据。
Ajax接口最优调用
页面往往依赖很多的异步接口,因此要对异步接口进行压测,找出接口的最优调用方式。如京东三级列表页依赖价格、库存、广告词、店铺信息等异步调用接口。而页面有时候会出现多达300多个商品,如果用一个get请求把这些sku做参数,性能非常慢,那么就要采用分组分批调用。如页面商品在300个时,价格接口分六组,第一组30个,第二组30个,第三组60个,第四组60个,第五组100个,第六组100个。
DNS预解析
对可能的域名进行提前解析,避免将来HTTP请求时的DNS延迟。如对价格、库存、图片、单品页等服务预解析。
减少HTTP重定向
HTTP 重定向极费时间,特别是不同域名之间的重定向,更加费时;这里面既有额外的DNS 查询、TCP 握手,还有其他延迟。最佳的重定向次数为零。比如三级列表页以前是http://list.jd.com/12-12-12.html,而现在是http://list.jd.com/list.html?cat=12,12,12;在过渡期间可以重定向,但是过渡完成后就没必要重定向了。
使用CDN(内容分发网络)
把数据放到离用户地理位置更近的地方,可以显著减少每次TCP连接的网络延迟,增大吞吐量。比如京东三级列表页、商品详情页、公共JS、CSS。
传输压缩过的内容(Gzip压缩)
传输前应该压缩应用资源,把要传输的字节减至最少:确保对每种要传输的资源采用最好的压缩手段。所有文本资源都应该使用Gzip压缩,然后再在客户端与服务端间传输。一般来说,Gzip可以减少60%~80%的文件大小,也是一个相对简单(只要在服务器上配置一个选项),但优化效果较好的举措。(对于压缩级别,经过不同服务器多次压测,建议Nginx设置为1-4)
去掉不必要的资源
任何请求都不如没有请求快,把一些非必须的或者可异步的,或者可延迟的尽量延迟请求。
在客户端缓存资源
应该缓存应用资源,从而避免每次请求都发送相同的内容。
无状态域名
Cookie 在很多应用中都是常见的性能瓶颈,很多开发者都会忽略它给每次请求增加的额外负担;减少请求的HTTP首部数据(比如HTTP cookie),节省的时间相当于几次往返的延迟时间。如列表页依赖的价格、库存接口,采用3.cn无状态域名,从而减少主域下cookie传输。
并行处理请求和响应
请求和响应的排队都会导致延迟,无论是客户端还是服务器端。这一点经常被忽视,但却会无谓地导致很长延迟。
域名分区
当页面中非常多请求都是一个域名下资源时,由于浏览器同时只能打开6个连接池,而且每个链接池是对不同域名起作用,所以很多请求一个域名会出现排队现象。如果把这些请求域名分区,让请求并行,从而加快资源下载。如:页面需要下载上百张图片,对图片进行域名分区调用。京东大部分页面都对图片进行了域名分区调用:
http://img10.360buyimg.com/
http://img11.360buyimg.com/
http://img12.360buyimg.com/
http://img13.360buyimg.com/
http://img14.360buyimg.com/
拼合和连接
合并链接:把多个JavaScript 或CSS 文件组合为一个文件。拼合:把多张图片组合为一个更大的复合的图片(CSS Sprites)。
服务端写相关信息到header
把服务器IP后两位写到header,如果有问题,方便定位哪台服务器。ups:后端路由的所有服务器都取到。把缓存命中信息或异常走兜底了,把后端运行状态写到header。Head-status:命中、未命中、异常等状态。
服务端架构
Nginx+Lua(OpenResty)+golang+redis缓存计算,后续再把列表页的架构整理出来。