关于SPA的SEO解决方案

shidong ke
13 min readApr 28, 2019

--

本文主要有三部分,首先介绍了为什么SPA需要SEO,以及流行的解决方案SSR及其弊端,最后介绍基于Prerender方式、SSR-free的SEO解决方案。

为什么SPA需要SEO

SPA全名是Single Page Application,指的是单页面应用。SEO全称为Search Engine Optimization,指的是搜索引擎优化。

SPA技术将产出html的逻辑从服务器转移到了客户端,在进入React, Vue等UI框架进行开发时,我们开发的页面更多的是在客户端进行脚本执行、数据请求和UI动态装载。

那么搜索引擎爬虫在抓取这样的页面的时,在未做任何优化的情况下,通常拿到的是类似下面的字符文本,

除了可以事先定义的title(可能title也不能事先确定),在SPA下很多内容需要通过ajax请求server拿到数据通过脚本执行产生。通常爬虫不会有类似浏览器的执行环境去产生这些内容。如何让爬虫拿到的数据和用户通过浏览器看到的数据尽量是一致,这就是本文的主要内容。

如何SEO

对于较小知情率的网站,需要搜索引擎导流,SEO能够提高用户访问率。那SEO有什么参考指标?

SEO可以分为2部分,On-Page和off-Page。

On-page SEO查看的是网站的内容
Off-page SEO 查看的是网站的权威性和流行性外部因素。

本文介绍的是对On-Page SEO,On-Page SEO主要有以下指标:

Title Tags
Headings (H1)
Fast-loading pages, or page load speed
Page content
URL structure

除了Page Content,其他的指标其实是一些开发上的要求、而不是框架上的选择。在基于React等UI框架开发上,我们都有针对On-page SEO中的Page Content做过什么SEO?

SSR,这真的是比较常听的做法。

为什么不推荐SSR

SSR十分擅长于动态内容的页面产出,但如果只是为了SEO,下一章节将介绍一种更通用、无痛的解决方案。如果你之前有用SSR,你就会赞同这句话:你有多爱它,就有多恨它。本章节将介绍SSR的产生,CSR(Client-side Render — 客户端渲染)、SSR(Server-side Render — 服务器端渲染)、Prerender(预渲染)的关系。并分析SSR在对比CSR和Prerender的优劣,以及引起本文基于Prerender方式的SEO解决方案。

前后端分离 vs SSR

SSR(也称为Server Side Script)是一个比较原始的前后端开发方式,伴随服务器端语言出现。例如php的网页的开发代码类似这样,获取数据库的数据或者通过服务端的计算然后输出到html模版中

在未出现ajax技术之前,前端页面是全量刷新的,自然最好的方式就是在服务器端就生成了关于内容的页面。随着浏览器的发展,JavaScript语言的发展、ajax等技术的出现,前端承担了越来越多的逻辑,开发方式也从SSR向CSR转变。一种开发理念也越来越深入到广大开发者中 — — 前后端分离,将服务和渲染分离。

但也出现了一些问题,之前服务器端将数据输出到html模版中再给用户的方式在目前的环境中拥有更多非开发方面的优点,不管是SEO、或是首屏渲染等;相反CSR则一般需要额外的操作才能获得真正有用的内容,比如JS请求和JS执行等。我想解决这些问题一定不是倒退回SSR,用发展的观点来看,技术发展将会让这些问题不再是问题。

但就目前的环境来说,过渡总是存在的,工程或商业上在一些场景仍然需要在CSR开发上兼容SSR,那么可以参考下nextjs,razzle等框架

SSR的优势

SSR不通过客户端的JS执行就已经拥有有意义的内容,文章The Benefits of Server Side Rendering Over Client Side Rendering有详细解读SSR的优势。主要集中在以下3点

  1. SEO和SMO(Social Media Optimization)
  2. First meaningful paint(首屏渲染)
  3. Graceful degradation(支持不支持或没有开启javascript功能浏览器)
Fig-1 相比SSR,CSR会在JS请求和执行(执行依赖请求的完成)完成才会有首屏渲染的内容,图片来自此文章

SSR的劣势

  1. 额外的开发和维护成本

我们需要在想支持SSR的一开始就要遵守一些规则,或者在项目的后期要要支持SSR,让需要检查你的代码,

1). 有没有全局mutable data,可能导致内存泄露

2). 有没有在SSR可能执行的代码中使用server没有而client端存在的api(window, document…,比如有没有在合适的生命周期(如react’s didMount…)请求数据)

3). 有没有使用Routeri18nRedux,HMR、react-loadablecss-in-js等第三方库需要在server端重新配置(观点来自文章)

对于之后开发可能引入的第三方库呢?使用前是否还要注意是否支持SSR?这对于开发来讲,同时考虑2个环境真的是太费脑了!

2. 服务端的开发

同时SSR还需要在服务器端开发,这一般也是前端的工作,因为运行的也是前端开发的代码,但对于大部分前端来说,一般没有丰富的服务器开发的经验,服务端为了性能可能需要考虑到缓存策略,如果为了能够很好地监控代码的运行情况,可能需要一些监控方面的开发等等。

3. 更多的服务端负载

服务端具有更多的负载,需要更多的机器、网络等方面的资源,不过这些相对开发人力资源方面来说还是微不足道的。SSR耗费更多的开发上和维护上的资源。下面我将介绍为什么SPA的SSR“同构”耗费更多人力资源的原因。

引起SPA的SSR“同构”痛点的深层原因

  1. 不同的执行环境

BOM(window…), DOM(document…), Ajax(XMLHttp​Request, fetch…), Web Workers…

这些属于浏览器环境的对象在node server环境下并不work。Tips for SSR

2. 不同的“执行上下文”

即使你的执行环境一样,如果server用浏览器来得到html,是不是这样就没有问题?如果Server 导出了一些异步渲染的节点的html呢?

这就相当于我们将服务器端的一个时间节点渲染的html转移到了客户端,但是如果没有将对应的状态数据迁移过来,会导致一些依赖发生变化,可能会发生奇怪的bug。所以不仅要将Server在该时间节点渲染的html同步到client,也需要将server的状态同步到client端。

但同步server和client的状态并不简单,如果server执行的是一个纯函数的渲染那倒十分简单,因为依赖相当简单,在同步状态方案中,就是假设将App中组件的状态依赖都转移到props中。但实际开发上一个App的函数是由各个复杂的组件构成,这些组件都可能不是纯函数,各个组件在运行的过程中都可能产生side-effect,并伴随着维护自身组件的状态。甚至如果要将组件的执行到异步渲染得到的html节点,那么就要保证直到该时间点上,所有的状态是“可追踪”的,然后将状态同步到客户端,用于重建出对应html节点的状态。但为了“可追踪”,我们需要做更多额外的事情。

目前大多数做法是尽量不执行到引起side-effect的逻辑,或者在执行到的时候将状态等价性地转移到App的props,因为为了“重建”出对应html对应的状态,我们一般需要“纯”<App>,无内部状态, 让props←-> App。将渲染html的组件们所需要的状态通过props传入<App>中。

SSR之于首屏渲染

在暂时不考虑SEO(本文最后将介绍一种通用的SEO解决方案)的情况下,SSR的最大优势就是首屏渲染了,不过SSR真的是首屏渲染的“瑞士军刀”了吗?

如果你的页面有做SSR,但首屏渲染的时间仍然很长,可能你要考虑下

  1. 有没有做Time to the First Byte,如果渲染该页面依赖后端耗时的计算和请求时间,那么可以考虑使用Streaming Server-Side Rendering
  2. 有没有阻塞性请求、有没有重定向请求
  3. 服务有没有靠近用户,由于SSR需要动态服务,不能CDN部署,可能在一些国家或地区的政策影响下,服务只能部署在A地,但用户却在B地

有没有比SSR更“前端”的方式,但又不失这些非开发上的优点,Prerender,更Static, 本文介绍的SEO方案就是一种Prerender的方式。

什么是Prerender

Prerender即为预渲染,预渲染不像服务器渲染那样实时输出 HTML,它是在用户访问之前就已经提前渲染好了html,比如React-Static库只在构建时为了特定的路由生成特定的静态html。

Prerender同时具有开发上和非开发上的优点

  1. 服务无关,不能具有丰富的后端开发经验,在利用类似React-Static库只在构建的时就渲染出了html文件
  2. Static, 静态文件具有SEO、首屏渲染等优点

但Prerender也不是万能的,相比SSR来说,

  1. SSR内容是动态的,PreRender在部署时就确定了内容
  2. SSR内容可以是用户相关的,PreRender是所有用户同一份内容

SSR更动态,Prerender更倾向于静态,所以Prerender很难覆盖到下面2个场景:
1. 永远穷尽不了预渲染所有页面,比如博客类网站、租房网站等类似B端发布的网站,在B端操作的过程中会产生新的页面。
2. 用户相关内容的首屏渲染问题无法解决

在一般的构建时Prerender开发方式中(非本文后面介绍的SEO解决方案使用Prerender方式)同样具有“引起SPA的SSR同构”的痛点,所以在SSR和Prerender开发中,我总结了三重境界。

SSR和Prerender的三重境界

第一重境界:昨夜西风凋碧树。独上高楼,望尽天涯路;

同步渲染html内容,指的是在执行JS脚本的同一个上下文中产生的html内容,一般这个较为简单,React提供的类似ReactDomServer.renderToString就可以达到这步,同时为html节点绑定了事件处理函数。

第二重境界:衣带渐宽终不悔,为伊消得人憔悴;

有时候我们可能需要异步请求服务器端的内容并输出到html中,利用类似React 组件开发的通用的方式是将组件异步请求的状态转换为整个App的输入(props)。

所以第二重境界可以说是异步计算html内容和维护对应状态

目前我们能做到的二重境界初级阶段:1. Server render尽量避免触发side-effect 2. 即使触发到side-effect,完整记录状态并传递给client端

第三重境界:众里寻他千百度,蓦然回首,那人却在灯火阑珊处;

异步计算html内容和keep“执行环境” ,最终的目的是为了client在解析html的同时能保持server渲染出该html所有依赖和side-effect。在不用额外同等意义上的状态转换,有没有更通用的方式去同步2端的“执行环境”。I don’t know…

机器的事情让机器来处理

上面简单分析了,CSR、SSR和Prerender之间的关系,最终来到本文的核心了。本文介绍SEO是一种Prerender方式,但与一般的Prerender开发方式不同的是,本文介绍的方案是基于puppeteer,puppeteer可以在服务器端运行浏览器环境,这也就解决了“SPA的SSR同构开发上的痛点”的第一个问题“不同的执行环境”,至于第二点 — — 不同的“执行上下文”,因为SEO的目的是提供爬虫有内容的问题,所以我们不需要继续执行之后的逻辑,所以也就不需要不同的“执行上下文”。

这个关于SPA的SEO解决方案可以简单地描述为,将爬虫引入到事先用puppeteer为SPA Prerender的内容服务中。

Fig-2 SPA的SEO解决方案示意图 Server for Request Filter是主要来区分爬虫和用户(基于UserAgent),Server for Crawlers是用于为SPA预渲染产生html内容

可以解决的问题有,
1. 已有的项目SEO
2. 新的项目为SEO, SSR-free,想用React SPA又不用为了使用SSR而苦恼

这种方案通用性强,在不侵入代码的同时适合绝大多数的网页SEO。

方案的功能和接口设计

1. 请求分流服务
需要一个用于请求分流的服务,或者设计成一个中间件

功能设计
1). 请求分流(爬虫 GET请求 非资源 白名单)// 白名单包含网址和对应的设置(内容优先还是速度优先) 比如格式为 [[‘/docment/*’, true], ‘/search’] tbd
2). 通知预渲染服务. 也将用户请求打到预渲染服务,用户可能来的比爬虫早
3). 透明代理(非中间件)
4). 请求预渲染服务并设置爬虫请求的Response. 对于满足条件的请求设置Response为Prerender内容

接口设计
1). 设置白名单,支持正则匹配。设置Response策略
2). 设置授权Token(对外使用)
3). 设置预渲染服务地址(协议,域名…)

2. 预渲染服务
基于puppeteer用于渲染SPA内容

功能设计
1. 鉴权(对外使用,配合请求分流服务)
2. 预渲染服务的爬取策略
有主动爬取和被动爬取
主动爬取是缓存过期时,主动爬取内容; 被动爬取是分析用户访问和爬虫,选择性爬取

3. 缓存策略
可以定义缓存时间,按照用户流量设置多级缓存

4. Response策略
内容优先还是速度优先,默认内容优先

5. 内容分析,服务优化
被动内容归类:multi-keys map (相似的域名多次渲染)
主动内容归类:设置规则 (增加维护成本)

6. 支持冷启动,http header配置X-prerender-cold(在没有“被动爬取”的用户访问时候启用)
SEO不仅将内容作为一个重要的参考指标,同时请求的往返时间也是很关键的指标。

所以需要缓存提前为搜索引擎爬虫做好内容准备,尽管我们通过分析用户请求尽量提前了缓存的时间,不过也很难保证一定在爬虫之前有缓存,如果没有缓存,同时又是“内容优先”的话,SEO的效果不会太好,可能就会遇到这样的问题:由于地址不可预测,很难为所有的(B端产生)网站做好缓存,冷启动(指的是在生成有效网页之后,请求页面)可以提前在B端产生有效的网址的时候,让Prerender服务提前缓存内容

接口设计
1. Request-Response调用,支持http-header、query string配置,配置内容包括Response策略配置,预渲染请求地址等

这种方案已经有了开源实现Prerender.io,上面描述的功能和接口设计并不完全和Prerender.io相同。

Prerender.io支持通过付费使用,也支持自己部署服务。开源的代码在github中,分流服务中间件预渲染服务

本文为SEO介绍了一种基于prerender方式的无侵入代码的选择,感谢阅读!

--

--