王尘宇王尘宇

研究百度干SEO做推广变成一个被互联网搞的人

Nuxt自适应SSR计划:SEO和首屏最小化优化

目前项目接纳 Nuxt SSR 来完成办事端衬着 ,为满足 SEO 需求,将非首屏内容也停止了恳求和办事端曲出,招致首屏时间变长(非首屏的资本恳求和组件的衬着城市带来额外开销)。关于海量的用户来说,少量的爬虫拜候需求反而影响了一般用户的拜候,招致 SEO 和用户体验提拔存在很大的矛盾。

为领会决那个问题,我们设想和理论了自适应 SSR 计划,来同时满足那两种场景的需求。今天会分享那个计划的手艺细节、设想思绪以及在施行该计划过程中碰到的一些相关的子问题的理论踩坑经历,欢送各人一路交换。

分享纲领

问题来源和布景问题处理思绪自适应 SSR 计划介绍接纳自适应 SSR 优化前后数据Vue SSR client side hydration 踩坑理论利用 SVG 生成骨架屏踩坑理论

问题来源和布景

目前项目接纳 Nuxt SSR 来完成办事端衬着,为满足 SEO 需求,将非首屏资本也停止了恳求和办事端曲出,招致首屏时间变长(非首屏的资本恳求和组件的衬着城市带来额外开销)

优化前的加载流程

目前我们的 Nuxt 项目接纳 fetch 来实现 SSR 数据预取,fetch 中会处置所有关键和非关键恳求

Nuxt 生命周期图

关于海量的用户来说,少量的爬虫拜候需求反而影响了一般用户的拜候,招致 SEO 和用户体验提拔存在很大的矛盾。

为领会决那个问题,我们希望能区分差别的场景停止差别的曲出,SEO 场景全数曲出,其他场景只曲出最小化的首屏,非关键恳求放在前端异步拉取

处理思绪

方案通过同一的体例来控造数据加载,将数据加载由专门的插件来控造,插件会按照前提来选择性的加载数据,同时懒加载一部门数据

判断是 SEO 情况,fetch 阶段施行所有的数据加载逻辑非 SEO 场景,fetch 阶段只施行最小的数据加载逻辑,比及页面首屏曲出后,通过一些体例来懒加载另一部门数据

优化后的项目影评页加载流程图

自适应 SSR 计划介绍

Gitlab CI Pipeline

自研 Nuxt Fetch Pipeline

借鉴 Gitlab CI 持续集成的概念和流程,将数据恳求设想为差别的阶段 (Stage ),每个阶段施行差别的异步使命(Job),所有的阶段构成了数据恳求的管线(Pipeline)

预置的 Stage

seoFetch : 面向 SEO 衬着需要的 job 集合,一般要求是全数数据恳求都需要,尽可能多的办事端衬着内容minFetch:首屏衬着需要的最小的 job 集合mounted: 首屏加载完之后,在 mounted 阶段异步施行的 job 集合idle: 空闲时刻才施行的 job 集合

每一个页面的都有一个 Nuxt Fetch Pipeline 的实例来控造,Nuxt Fetch Pipeline 需要设置装备摆设响应的 job 和 stage,然后会自适应判断恳求的类型,针对性的处置异步数据拉取:

若是是 SEO 场景,则只会施行 seoFetch 那个 stage 的 job 集合若是是实在用户拜候,则会在办事端先施行 minFetch 那个 stage 的 job 集合,然后立即返回,客户端能够看到首屏内容及骨架屏,然后在首屏加载完之后,会在 mounted 阶段异步施行 mounted stage 的 job 集合,别的一些优先级更低的 job,则会在 idle stage 也就是空闲的时候才施行。

Nuxt Fetch Pipeline 利用示例

page 页面 index.vue

import NuxtFetchPipeline, {pipelineMixin,adaptiveFetch,} from @/utils/nuxt-fetch-pipeline;import pipelineConfig from ./index.pipeline.config;const nuxtFetchPipeline = new NuxtFetchPipeline(pipelineConfig);export default {mixins: [pipelineMixin(nuxtFetchPipeline)],fetch(context) {return adaptiveFetch(nuxtFetchPipeline, context);},};

设置装备摆设文件 index.pipeline.config.js

export default {stages: {// 面向SEO衬着需要的 job 集合,一般要求是全数seoFetch: {type: parallel,jobs: [task1]},// 首屏衬着需要的最小的 job 集合minFetch: {type: parallel,jobs: []},// 首屏加载完之后,在 mounted 阶段异步施行的 job 集合mounted: {type: parallel,jobs: []},// 空闲时刻才施行的 job 集合idle: {type: serial,jobs: []}},pipelines: {// 使命1task1: {task: ({ store, params, query, error, redirect, app, route }) => {return store.dispatch(action, {})}}}}

并发控造

Stage 施行 Job 撑持并行和串行 Stage 设置装备摆设 type 为 parallel 时为并行处置,会同时起头每一个 job 期待所有的 job 完成后,那个 stage 才完成 Stage 设置装备摆设 type 为 serial 时为串行处置,会依次起头每一个 job,前一个 job 完成后,后面的 job 才起头,最初一个 job 完成后,那个 stage 才完成

Job 嵌套

能够将一些能够复用的 job 定义为自定义的 stage,然后,在其他的 Stage 里根据如下的体例来引用,削减编码的成本

{seoFetch: {type: serial,jobs:[getVideo,{ jobType: stage, name: postGetVideo }]},postGetVideo: {type: parallel,jobs: [anyjob,anyjob2]}}

Job 的施行上下文

为了便利编码,以及削减改动成本,每一个 job 施行上下文和 Nuxt fetch 类似,而是通过一个 context 参数来拜候一些形态,因为 fetch 阶段还没有组件实例,为了连结同一,都不成以通过 this 拜候实例

目前撑持的 nuxt context 有

approutestoreparamsqueryerrorredirect

Stage 的划分思绪

Stage

合适的 Job

能否并行

seoFetch 全数,SEO 场景逃求越多越好

更好并行

minFetch 关键的,好比首屏内容、核心流程需要的数据,页面的次要核心内容(例如影评页面是影评的注释,短视频页面是短视频信息,帖子页面是帖子注释)的数据 更好并行

mounted

次关键内容的数据,例如侧边栏,第二屏等

按照优先成都考虑能否并行

idle

最次要的内容的数据,例如页面底部,标签页被隐藏的部门

尽量分批停止,不影响用户的交互 利用 SVG 生成骨架屏踩坑理论

因为办事端只拉取了关键数据,部门页面部门存在没有数据的情况,因而需要骨架屏来提拔体验

Vue Content Loading 利用及原理

例子

import VueContentLoading from vue-content-loading;export default {components: {VueContentLoading,},};

Vue Content Loading 核心代码

const validateColor = color =>/^([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/.test(color);export default {name: VueContentLoading,props: {rtl: {default: false,type: Boolean,},speed: {default: 2,type: Number,},width: {default: 400,type: Number,},height: {default: 130,type: Number,},primary: {type: String,default: f0f0f0,validator: validateColor,},secondary: {type: String,default: e0e0e0,validator: validateColor,},},computed: {viewbox() {return `0 0 ${this.width} ${this.height}`;},formatedSpeed() {return `${this.speed}s`;},gradientId() {return `gradient-${this.uid}`;},clipPathId() {return `clipPath-${this.uid}`;},svg() {if (this.rtl) {return {transform: rotateY(180deg),};}},rect() {return {style: {fill: url( + this.gradientId + ),},clipPath: url( + this.clipPathId + ),};},},data: () => ({uid: null,}),created() {this.uid = this._uid;},};

SVG 动画卡顿

利用了 Vue content loading 做骨架屏之后,发如今 js 加载并施行的时候动画会卡住,而 CSS 动画大部门情况下能够离开主线程施行,能够制止卡顿

CSS animations are the better choice. But how? The key is that as long as the properties we want to animate do not trigger reflow/repaint (read CSS triggers for more information), we can move those sampling operations out of the main thread. The most common property is the CSS transform. If an element is promoted as a layer, animating transform properties can be done in the GPU, meaning better performance/efficiency, especially on mobile. Find out more details in OffMainThreadCompositing. https://developer.mozilla.org/en-US/docs/Web/Performance/CSSJavaScriptanimation_performance

测试 Demo 地址

https://jsbin.com/wodenoxaku/1/edit?html,css,output

看起来阅读器并没有对 SVG 动画做那方面的优化,最末,我们修改了 Vue content loading 的实现,改为了利用 CSS 动画来实现闪灼的加载效果

const validateColor = color =>/^([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/.test(color);export default {name: VueContentLoading,props: {rtl: {default: false,type: Boolean,},speed: {default: 2,type: Number,},width: {default: 400,type: Number,},height: {default: 130,type: Number,},primary: {type: String,default: F0F0F0,validator: validateColor,},secondary: {type: String,default: E0E0E0,validator: validateColor,},uid: {type: String,required: true,},},computed: {viewbox() {return `0 0 ${this.width} ${this.height}`;},formatedSpeed() {return `${this.speed}s`;},clipPathId() {return `clipPath-${this.uid || this._uid}`;},style() {return {width: `${this.width}px`,height: `${this.height}px`,backgroundSize: 200%,backgroundImage: `linear-gradient(-90deg, ${this.primary} 0, ${this.secondary} 20%, ${this.primary} 50%, ${this.secondary} 75%, ${this.primary})`,clipPath: url( + this.clipPathId + ),animation: `backgroundAnimation ${this.formatedSpeed} infinite linear`,transform: this.rtl ? rotateY(180deg) : none,};},},};@keyframes backgroundAnimation {0% {background-position-x: 100%;}50% {background-position-x: 0;}100% {background-position-x: -100%;}}

Vue SSR client side hydration 踩坑理论

一个例子

text: {{ id }}export default {data () {return {id: Math.random()}}}

client side hydration 的成果会是若何呢?

A. id 是 client 端随机数, text 是 client 端随机数B. id 是 client 端随机数, text 是 server 端随机数C. id 是 server 端随机数, text 是 client 端随机数D. id 是 server 端随机数, text 是 server 端随机数

为什么要问那个问题 ?

Vue content loading 内部依赖了 this._uid 来做为 svg defs 里的 clippath 的 id,然而 this._uid 在客户端和办事端其实不一样,现实跟上面随机数的例子差不多。

client side hydration 的成果是 C

也就是说 id 并没有改动,招致的现象在我们那个场景就是骨架屏闪了一下就没了

什么会呈现那个情况?

初始化 Vue 到最末衬着的整个过程

来源:https://ustbhuangyi.github.io/vue-analysis/data-driven/update.html%E6%80%BB%E7%BB%93

所谓客户端激活,指的是 Vue 在阅读器端接收由办事端发送的静态 HTML,使其变成由 Vue 办理的动态 DOM 的过程。

在 entry-client.js 中,我们用下面那行挂载(mount)应用法式:

// 那里假定 App.vue template 根元素的 `id="app"`app.$mount(app);

因为办事器已经衬着好了 HTML,我们显然无需将其丢弃再从头创建所有的 DOM 元素。相反,我们需要"激活"那些静态的 HTML,然后使他们成为动态的(可以响应后续的数据变革)。

若是你查抄办事器衬着的输出成果,你会留意到应用法式的根元素上添加了一个特殊的属性:

data-server-rendered 特殊属性,让客户端 Vue 晓得那部门 HTML 是由 Vue 在办事端衬着的,而且应该以激活形式停止挂载。留意,那里并没有添加 id="app",而是添加 data-server-rendered 属性:你需要自行添加 ID 或其他可以拔取到应用法式根元素的选择器,不然应用法式将无法一般激活。

留意,在没有 data-server-rendered 属性的元素上,还能够向 $mount 函数的 hydrating 参数位置传入 true,来强迫利用激活形式(hydration):

// 强迫利用应用法式的激活形式app.$mount(app, true);

在开发形式下,Vue 将揣度客户端生成的虚拟 DOM 树 (virtual DOM tree),能否与从办事器衬着的 DOM 构造 (DOM structure) 婚配。若是无法婚配,它将退出混合形式,丢弃现有的 DOM 并从头起头衬着。在消费形式下,此检测会被跳过,以制止性能损耗。

vue 关于 attrs,class,staticClass,staticStyle,key 那些是不处置的

list of modules that can skip create hook during hydration because they are already rendered on the client or has no need

uid 处理计划

按照组件生成独一 UUID

props 和 slot 转换为字符串hash 算法

太重了,放弃

最末处理计划

痛快让用户本身传 ID

优化效果

通过削减 fetch 阶段的数据拉取的使命,削减了数据拉取时间同时削减了办事端衬着的组件数和开销,缩短了首字节时间首屏大小变小也缩短了下载首屏所需的时间

综合起来,首字节、首屏时间都将提早,可交互时间也会提早

当地数据

类型

办事响应时间 首页大小 未 Gzip 首页修改前

0.88s

561 KB

首页(最小化 fetch 恳求) 0.58s

217 KB

在当地测试,办事端衬着首页只恳求关键等办事器接口恳求时,办事响应时间缩短 0.30s,降低 34%,首页 html 文本大小降低 344 KB,削减 60%

线上数据

首页的首屏可见时间中位数从 2-3s 降低到了 1.1s 摆布,加载速度提拔 100%+

总结

本文分享了若何处理 SEO 和用户体验提拔之间存在矛盾的问题,介绍了我们若何借鉴 Gitlab CI 的 pipeline 的概念,在办事端衬着时兼顾首屏最小化和 SEO,分享了自适应 SSR 的手艺细节、设想思绪以及在施行该计划过程中碰到的一些相关的子问题的理论踩坑经历,希望对各人有所启发和帮忙。

关于我

binggg(Booker Zhao) @腾讯- 先后就职于迅雷、腾讯等,小我开源项目有 mrn.js 等- 兴办了迅雷内部组件仓库 XNPM ,参与几个迅雷前端开源项目标开发- 热衷于优化和提效,是一个奉行懒惰使人前进的懒人工程师

社交材料

GitHub: https://github.com/binggg简书: https://www.jianshu.com/u/60f22559b79f掘金: https://juejin.im/user/58d31f130ce4630057edb3ba️‍️ 微博: https://weibo.com/being99思否: https://segmentfault.com/u/binggg博客园: https://www.cnblogs.com/binggg/开源中国: https://my.oschina.net/u/4217267极术社区: https://aijishu.com/u/binggg今日头条: https://www.toutiao.com/c/user/102306299647CSDN: https://blog.csdn.net/weixin_42541867

微信公家号 binggg_net, 欢送存眷

相关文章

评论列表

发表评论:
验证码

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。