2017-02-04 11:33:18

二月随手记

SSR歪门邪道之:服务器完全直出

比较SSR与传统MVC框架直出的HTML,可以发现有两处不同:

  • SSR页面有初始化全局状态的数据
    • 比如本页面的window.__INITIAL_STATE_
  • SSR页面末尾有script标签,用以加载整个web app
    • 比如本页面的app.[hash].js

因此,只要在SSR服务端上,将window.__INITIAL_STATE_和末尾的script标签去除,你的页面就完全变成了MVC服务器直出的页面了。

优点是加快首屏访问,缺点是拖慢了非首屏访问并大幅度降低qps,毕竟SSR时跑的是vm,1核1G的机器压测出来的QPS也就惨不忍睹的几十。

因此除非对首屏访问的毫秒数敏感到丧心病狂了,否则没人会想到并真的这样做

SSR与hash

SSR服务端有一个缺点,它是拿不到用户访问的hash的,比如https://smallpath.me/post/2017-02-cut-hand#ssr与hash#SSR与hash,因为客户端甚至不会发送这个片段,这也是路由使用hash模式的SPA可以拦截浏览器路由的原因

hash对Table Of Contents(TOC,说白了就是目录)的跳转比较重要,因此需要在客户端将首屏数据中的hash给补充回来,避免丢失导致hash跳转失败

window.__INITIAL_STATE__.route.hash = window.location.hash
store.replaceState(window.__INITIAL_STATE__)

上面的代码之所以生效,是因为使用了vue-router-sync,因此全局状态的route对象可以更改路由。

依此,给vue-hackernews-2.0提了一个PR

又被版本给坑了

弄SSR的开发热重载,搞了半天vue-loader一直在报错,说传过来的loader不合法。

马上克隆好vue-hackernews-2.0做对照组,立刻就发现我把仓库的vue-loader给锁到10.0.0了,但是webpack插件没锁,导致未匹配,我勒个去。

版本这玩意儿真心坑啊,真要锁版本看来必须全部锁掉

log4js

[[Promise]].catch(log.error) // Wrong!!

这是一种完全错误的写法,log.error中的this已经被更改,不会指向log4js了,非常容易导致报错信息未写入生产日志。

要么绑this,要么给回调,在JS中偷懒十分容易碰到ES5甚至ES3的坑。

[[Promise]].catch(log.error.bind(log))
 [[Promise]].catch(err => log.error(err)))

hash跳转被fixed header覆盖

在小屏上显示时,通过hash跳转的位置会被position为fixed的header覆盖住。

这个问题是默认的hash跳转由浏览器控制,而大多数浏览器的跳转实现完全无视fixed header的高度,导致高度计算错误。

有如下几种解决办法

纯css

这种情况只适合只有标题需要hash跳转的情况

  a :target :before {
    padding-top: 50px;
  }

覆盖window.onhashchange

重写这个回调后,hash跳转已经可以由我们自行制定了,缺点是需要自行计算目标的位置,会大幅度侵入前端JS代码

生成隐藏锚点

这个办法需要在生成HTML时增加隐藏锚点,一般Markdown生成HTML都是在后台完成的,因此前端改动很小,只需要加两三行css而已,综合来看是最好的办法

修改后台编辑器的marked,增加隐藏的anchor

const renderer = new Marked.Renderer()
export const toc = []

renderer.heading = function (text, level) {
  var slug = text.toLowerCase().replace(/\s+/g, '-')
  toc.push({
    level: level,
    slug: slug,
    title: text
  })
  return `<h${level}><a href='#${slug}' id='${slug}' class='anchor'></a><a href='#${slug}'>${text}</a></h${level}>`
}

Marked.setOptions({
  renderer
})

在前端中增加样式及媒体查询

a.anchor {
  display: block;
  position: relative;
  visibility: hidden;
}

@media screen and (max-width:768px) {
  a.anchor {
    top: -50px;
  }
}

前端框架的同质化

拿react,vue,angular来举例,各自都有虚拟DOM,JSX等等等等,很少有核心不同的地方。但是,细微的不同之处反而可以造成了完全不同的结果。中文圈最近一直是推崇vue大于react,博主现在也是更多在用vue开发前端项目,但是,博主反而更觉得react会比vue走得更远

vue服务端渲染的性能

以使用了vue服务端渲染的本博客为例,1核1G内存仅仅能压出54QPS,单个请求平均需要花费20ms,这是什么概念?

这还是在博主使用了vue组件级缓存以及缓存请求的情况下,也就是说,实际上只有第一个请求完整地跑完了服务端渲染的整个流程,压测的其他请求全部是直接拿的缓存下来的HTML代码。

连缓存请求这种杀敌一千自损八百的手段都上了,都仅仅只有54的QPS,实在是惨不忍睹,惨无人道,惨绝人寰,作为一个后端实在是觉得这QPS不要太惨好吗。

作为对比,就相当于后端使用模板引擎拼出HTML,而且只在第一次请求时进行拼接流程并进行缓存,接下来其他请求全部直接拿缓存因此开销低得可怜,就算翻十倍翻到540QPS也是很简单就能办到的。

什么拖慢了QPS呢?锅不能扔给虚拟DOM,即使它本来就很慢;而是该扔给vue的bundleRender。

vue的bundleRender为了不让一个请求产生的全局状态影响其他请求,因此使用了node的vm模块来进行加载,vm模块是很慢很慢的,初始化时很容易超过10ms,这也是为什么SSR会出现54QPS这种十位数的奇葩数据。

而为什么一个请求产生的全局状态会影响其他请求的结果呢?因为vue的状态管理不是immutable的!而对于使用redux+immutable.js的react来说,虽然上手门槛确实高多了,但是react的服务端渲染完全就不用做bundleRender不用使用vm模块了,immutable数据做时间旅行都行了,根本就不会影响其他请求。

因此,vue的服务端渲染性能是远远比不上react的。

2017-05-02更新

经过三个月的持续优化以及vue官方对ssr的性能提升,现在ssr的性能可以说已经非常好了,在不上缓存请求的情况下,本博客已经可以轻松地在1核1G的机器上压出50QPS。具体的优化过程如下:

  • 推算API路由后直连数据库
    • 单请求从40ms降低到25ms
  • vue2.3.0的ssr优化
    • 导出工厂函数后,单请求从25ms降低到了20ms

如上这两种是属于后端的优化,可以从压测来得到具体的数据,还有另外几种优化前端文件大小的策略,不好压测,但是效果丝毫不差上面这两种后端优化

  • 服务端化谷歌统计
    • 从190kb降低到160kb
  • tree-shaking
    • 从160kb降低到140kb
  • 将所有路由全部按需分块加载
    • 从140kb降低到120kb
  • 造ajax库和状态管理库
    • 从120kb降低到105kb
  • 配合service-worker做强缓存
    • 提前异步拉取未使用到的资源,可以将路由全部按需分块加载这个策略的缺点完全解决

可以看到,首屏文件大小经过优化后从190kb降低到了105kb,减小了45%的大小,首屏DOMContentLoad低于150ms,并且完全不影响博客的任何功能。

node v7.6.0

async/await来了!

Markdown图片高度的问题

目前,Markdown并没有一个统一的设置图片高度的标准。

然而,![]()形式的图片在页面初始化时高度为0,当图片加载好时会恢复真实高度导致闪屏,另外会使TOC跳转到错误的位置。

因此,需要手动将![]()形式的托转为<img height="960" src="url">来指定高度,避免闪屏。

幸运的是,大多是静态图片存储商都提供API以供查询图片信息,以七牛云为例,给图片加上?imageInfo后缀,即可返回包含图片高度的图片信息。

本文链接:https://smallpath.me/post/2017-02-cut-hand

-- EOF --