亿万级访问量下的前端同构直出实践

背景

兴趣部落项目自2014年至今,一直都是采用的是前端渲染的模式,这种模式就是页面html是一个空壳,首屏的内容需要css和js都加载完成后,请求cgi获得数据后再渲染给用户。这种模式的好处是可以让后端和前端的工作完全分离,给日常的开发和维护带来很大的便利。

我们在现在的工作模式上,为了尽可能的减少首屏耗时,做了相当多的优化,包括使用离线包的机制来减少css和js的时间

但是这些所有的优化,仍然是基于JS执行后,才可以向用户交付首屏的,如果遇到android上执行JS速度很慢的机器,就会显得耗时仍然特别长。

使用直出的页面,html不再只是一个空壳,而是一个渲染良好的页面,这样用户就可以不用等待JS加载和执行后看到内容,大大减少用户的焦虑感。

在现有的工作模式下,使用同构直出的手段,不仅可以保留我们现有的开发模式,还可以减少很多工作量。试想,我们现在将现有的工作模式全部推翻使用普通直出,要面临多少工作重建。

同构直出,前后端完全使用同一套代码,将前端的渲染逻辑移到服务器端完成,将渲染后的结果再交给用户,得益于React这套体系,我们将这样的能力应用到了兴趣部落项目中。

历程

首先我们先在一个小页面上进行全量尝试,不断解决,调整其间遇到的问题

后面,待我们的架构成熟了之后,我们把这套体系运用到兴趣部落的三大核心页面之一的帖子详情页

经过不断的灰度、解决问题,最终帖子详情页的同构直出正式全量上线

其中的机器,几乎全是v4虚拟机。

效果

直观感受下对比的效果,如下图

左侧为前端渲染,右侧为同构直出

可以明显看出,直出在android机型下带来的优化效果是非常明显的,同时从正常的测速数据上来看,直出的首屏耗时减少了50%,慢速用户占比减少了3个百分点

架构

前端代码的架构是传统是react+redux架构体系,使用redux的架构可以让我们的直出更可控

挑战

内存问题

同构直出大部分情况下都要面临此类问题,普通的前端页面极少会考虑内存泄露的原因,然而在node端运行的代码都要考虑内存泄露的问题。

一次用户访问的管道中,res.end()调用完了,理论上管道产生的内存可以完全被回收,如果不可以被回收,那么就会产生内存一直增长的问题。

我们都知道,挂到GC ROOT上的变量都无法回收,前端的代码太多不控的代码会导致内存泄露,我们需要一个通用的解决方案

原来的代码

let Main = require(MainEntry);

虽然每个请求,每个用户都会去require同构的Main组件,但是由于node端require是单例模式,所以每个用户引用的Main都是同一个引用,每个请求对Main(Main的执行)内部产生的变量声明,如果该变量连接到Main的引用链上的,当用户请求结束的时候是无法释放的,因为Main的引用是单例的,会node缓存住,所以这些变量就无法回收,会产生严重的内存泄露问题。

上线时内存暴涨的问题

为了解决这个问题,可以对每个用户请求,开辟一个新的Main实例,这样当用户请求结束了,Main的引用可以被顺利回收,就不会产生内存泄露的问题

目前部落中使用的是vm的解决方案,为每个用户请求创建了一个沙箱环境

if(! mainVmScriptCache[entry]){
    var code = fs.readFileSync(require.resolve(entry), 'utf8');

    mainVmScriptCache[entry] = new vm.Script(m.wrap(code), {
        filename: entry
    });
}

//var startVmTime = + new Date();
var module = {
    exports: {}
};

var exports = module.exports;

mainVmScriptCache[entry].runInThisContext()(exports, require, module, __filename, __dirname);

Main = exports.default;

每个用户请求过来,都会重新变编译出一个Main, 这个Main引用不与其他请求共享,请求结束了,Main也会被回收,Main中产生的所有垃圾内容都会被一起回收

内存得到有效控制

关于性能问题,vm产生的性能会带来CPU的使用耗时增加,大约20ms,但对内存控制是非常有效的。

关于这块的优化,同构直出本来就是一个CPU密集型的任务,后续可以结合缓存来将CPU密集型任务转为内存密集任务

二次CGI

虽然解决这个问题的方案并不难,但重在我们能在详情页放量前能发现这个常常被忽略的问题。

通用的重构直出方案,到前端的代码会正常执行,这样cgi会在前端再发一次,数据也会变成最新的。但是,实际上,服务器端已经为该用户发一次请求了,这样就导致了一个用户请求了两次cgi。

这里的方案通常可以划为优化的角度去考虑。

在第一个小页面上线的时候,我们并没有太重视这个问题,但是详情页灰度上线的时候,我们逐渐认识到这不是一个优化问题,而是一个严重的架构问题。如果详情页直接上线,对后台cgi带来量的冲击是非常大的,原本3亿的日访问量一下子变成6亿的访问量,这比30w变成60w对后台的压力要远远大的多。所以这个问题要在继续放量前必须解决的问题。

解决的方案就是使用数据cache,将node端已请求的数据同时吐到前端去,这样在前端请求的时候做一次拦截,检查是否有数据缓存,如果有的话就不再请求CGI, 这样可以大大消除新增CGI的量

但是遇到的问题,数据用url_参数做key存储的时候,往往因为前后端不一致的参数导致缓存无法匹配,比如前端使用了地理位置信息参数,这个在服务器端是无法换取到的。解决的方案就是将这些参数存到cookie里,请求的时候node端可以用cookie缓存的位置信息数据

(客户端依赖参数使用cookie,缓存命中率大大提高)

离线包

css资源、js资源使用离线包是比较想当然的事情,但是在部落转为直出,接入离线包也遇到一些困难。

(js、css md5值很多)

用户端的离线包版本是很多的,每个离线包版本对就没的资源的md5又不一样,直出的页面引用的资源又该怎么知道用户本地离线包的md5是哪个呢?

我们使用了如下的解决方案

在前端编译离线包的时候,会把html内注入一段script,script作用是在当前页面下种下一个代表版本号的数据(version),同时将此html命名成version.html发送到直出服务器,那入由该离线包发出的直出请求都会带上这个版本信息,我们根据这个版本信息将对就的version.html做为本次直出要吐出页面的模板,这样到用户端可以匹配到用户离线包的资源。

首屏优先

首屏优先也是常常被大家忽略的体验问题,大部分前端渲染的页面都是如下的样子

<html>
<head>
  <link href="main.css" />
</head>
<body>
  <div class="root"></div>


  <script src="lib.js"></script>
  <script src="render.js"></script>
</body>
</html>

如果使用直出会变成这个样子

<html>
<head>
  <link href="main.css" />
</head>
<body>
  <div class="root">
        <h1>title</h1>
        <div class="content">content</div>
  </div>

  <script src="lib.js"></script>
  <script src="render.js"></script>
</body>
</html>

看起来也没什么问题,内容直接出现在.root里了

但是我们经常会忽略一个体验问题,这样的页面真的是会比非直出快么?

答案是80%否定的!也不是大部分情况并不会比非直出快!甚至体验上会比非直出更慢!

原因是要弄清楚浏览器首屏的出现时机,什么时候浏览器会执行第一次paint ? 简单来讲,大部分情况下直出的dom元素并不会第一时间展示出来,而是等render.js执行完,才会展示首屏内容,如果render.js都加载并执行完,那么我们直出的dom元素还有什么意义,这又回到普通的前端渲染了,空壳架子又比原来还要多了,所以难免白屏时间会更长。

所以为了解决这个问题,我们要让直出的dom节点可以第一时间展示出来,解决的方法也不难,可以使用懒加载,部落使用了更好async方案,第一时间展示首屏内容,第一时间加载JS,并且不阻塞DOM渲染,不阻塞首屏交付。

感谢x5内核同学weetli的指导

关于首屏渲染时间:

  • css会阻塞渲染(paint) (css没有加载完成渲染没有意义)
  • js会阻塞文档解析,不会阻塞渲染
  • 浏览器解析到script标签时,如果js资源已经准备好了,会先执行js,再做渲染,如果没有执行好会先渲染
  • 大部分线上的cdn资源都是有强缓存的,或者有手Q离线包,浏览器解析到script标签时js资源已经准备好,会先执行js,再做渲染

首屏渲染的时机涉及么很多因素,很不可控,但是x5内核浏览器提供给了便利的控制方法来优化首屏时机

x5首屏渲染时机可以自己定义,添加meta标签

<meta name="x5-pagetype" content="optpage">

x5-pagetype有三种可选类型

  • default 自动首屏探测
  • optpage 首屏标签
  • webapp 不做探测

首屏标签为

<first-screen/>

维稳

一个线上的后台任务,最大的问题就是讲稳定和容灾,首先任务保证用户的服务是稳定的,遇到一些突发问题时候,线上的页面仍然可以稳定的提供服务。

相比传统的直出,同构拥有更强的容灾的能力,这也同构直出的魅力所在!因为在同构直出宕掉的时候,还有前端渲染页面可以提供正常的服务,所以部落在部署页面的存在两种模式

现有的前端渲染路径:https://buluo.qq.com/mobile/detail.html

对应的直出页面路径: https://buluo.qq.com/mobile/v2/detail.html

比如这个直出页面http://buluo.qq.com/mobile/v2/detail.html?bid=227061&pid=2056550-1495770696&_wv=1027&webview=1 (模拟器打开),去掉v2就是非直出页面http://buluo.qq.com/mobile/detail.html?bid=227061&pid=2056550-1495770696&_wv=1027&webview=1 (模拟器打开)

兴趣部落直出项目在容灾策略上提供了两层容灾策略

第一层 框架层 · 超时、出错容错

框架超时、出错时候就会返回一个页面原始的非直出html页面,这样到用户端就可以走正常前端渲染。

第二层 运维层 · 服务宕机容错

这一层的容错会放在服务机的前置层,简单来讲就是请求直出页面出现5xx、4xx的错误,就会隐式的转发路径到不含v2的非直出页面。

   location ^~ /mobile/v2/ {
        proxy_pass xxx;
        proxy_intercept_errors on;
        error_page 403 404 408 500 501 502 503 504 @buluo_static_page;
    }

    location @buluo_static_page {
        rewrite /v2/(.*)$ /mobile/$1 last;
    }

即使整个直出服务完全挂掉,我们都不用担心服务的可用性

自动化测试

另个一个层次,如何保证平时开发过程的稳定性,也是整个架构体系重要的一环,不要等到有问题的代码的发到线上才发现有问题。

部落在直出的开发维稳体系上,首次引入的了自动化测试+git hook的方案来保证提交的代码一定是不会出问题的。

其他同学提交的代码在push的时候会触发本地prepush hook并进行直出页面的自动化测试,只有通过自动化测试才可以提交代码。

这个方案极大的保证了直出服务的稳定,自此方案上线以来,再无直出服务出现问题情况发生~

展望

应用型技术的难点不是在克服技术问题(因为大部问题都是有解决方案的),而是在于能够不断的结合自身的产品体验,发现其中存在的体验问题,不断使用更好的技术方案去优化用户的体验,为整个产品发展添砖加瓦。

做为公司最大的同构直出服务实践,在后续的方案中,我们会进一步着手优化用户的使用体验。比如使用服务器缓存等手段来进一步减少服务器端的耗时,优化直出图片的加载的体验等等,同时会更多丰富的实战经验分享给大家。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

王斌的专栏

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏互联网杂技

移动端开发的一些技巧

开篇语 最近接手了一个移动端的项目。个人感觉是自己做得比较快而且比较健壮的一个。。。移动端最主要就是页面要适用不同的手机屏幕,ipad等。下面就分享一些技巧,让...

26310
来自专栏全华班

认识工作流-Activiti详细说明

阅读文本大概需要 5 分钟。 一、Activiti详细说明 ? 首先给大家介绍一下BPMN2规范的分类分为几个部分。 1启动与结束事件、2顺序流、3任务、4网...

3608
来自专栏DannyHoo的专栏

网络请求为什么要使用第三方库???

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

943
来自专栏MessageQueue

如何在MQ中实现支持任意延迟的消息?

定时消息与延迟消息在代码配置上存在一些差异,但是最终达到的效果相同:消息在发送到 MQ 服务端后并不会立马投递,而是根据消息中的属性延迟固定时间后才投递给消费者...

2124
来自专栏腾讯移动品质中心TMQ的专栏

从插件重构看如何提升测试质量与效率

几个月前技术侧发起了一轮手机管家小火箭的重构,目的是为了更好地梳理小火箭的代码架构逻辑,方便以后更好地提高开发效率和开发质量。

2856
来自专栏包子铺里聊IT

5分钟 Hadoop Shuffle 优化

上篇5分钟深入 Hadoop 的文章中,我们介绍了如何优化输入处理,让 Hadoop 达到更高的性能;另一个有可能让 Hadoop 性能实现质的飞越的过程是 S...

3105
来自专栏程序员宝库

Hybrid App技术解析 -- 原理篇

随着 Web 技术和移动设备的快速发展,Hybrid 技术已经成为一种最主流最常见的方案。一套好的 Hybrid架构方案 能让 App 既能拥有极致的体验和性能...

972
来自专栏大闲人柴毛毛

浅谈代码覆盖

在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或 90%。于是乎,测试人...

3306
来自专栏向治洪

不可错过的Node.js框架

前言 Node.js是由Ryan Dahl于2009年创建的。它是一个开源的跨平台运行时环境,用于开发服务器端和网络应用程序,它是基于Google Chrome...

35510
来自专栏后端技术探索

高并发高性能分布式框架从无到有微服务架构设计分享

微服务架构模式(Microservice Architect Pattern)。近两年在服务的疯狂增长与云计算技术的进步,让微服务架构受到重点关注。

771

扫码关注云+社区