ITPub博客

首页 > IT基础架构 > 服务器/存储 > ES和SSG在得物软广业务上的实践

ES和SSG在得物软广业务上的实践

服务器/存储 作者:架构师修行手册 时间:2024-01-11 09:42:58 0 删除 编辑

来源:得物技术

目录

一、背景

二、技术选型

三、页面性能优化

    1. 自定义文档行为

    2. 打包优化

    3. 首屏资源动态化

    4. 图片资源优化

    5. Http2.0下更优雅的拆包

    6. 第三方包体积优化

四、上线前的准备

五、页面级别的灰度AB

六、全量的灰度收益

    1. 性能前后对比

    2. 业务前后对比

七、灰度方案

八、ES

九、灰度节奏

十、灰度时间安排

十一、稳定性


一

背景


目前软广自营+商投渠道,每个季度的消耗都不少,并且在不停地铺开多媒体平台,软广的重要性不言而喻。运营人员对于编辑器的编辑概念已经弱化(软广运营创建会场的频率不高),目前都是通过固定几个外投链接,然后通过在投放链接拼接上增长算法侧的捞月 ID 参数,来创建很多的外投广告计划。

所以针对业务的变化,运营侧迫切希望目前软广外投的页面打开速度得到优化,以及解决会场失效等一些高频问题,同时与现有的投放广告系统做充分的融合,减少中间无意义的 Gap。


项目收益


这里我们选取技术指标 FCP_Rate FCP 的数值小于 1 秒内的数值占总 PV 的比例,用于衡量我们这次新版落地页的性能收益。

Lighthouse 跑分对比:慢速的情况下从之前的 56 分提高到 79 分,网速较快的情况下,从之前的 87 分 提高到 98 分。

真实数据收益:老版的 FCP_Rate 平均在 35% 左右,新版上线以来,FCP_Rate 平均在 80% 左右。提高了 45pp 左右。


二

技术选型


在做技术选型之前,我们仔细分析了目前现有外投软广的 C 端页面,其中流量 top3 就是九宫格、单品图、商品流(如下图所示)。整个外投软广的流量高峰期能够达到 100多万PV 以上,而这 3 个模板的流量几乎占据了 90% 以上,所以对高频软广页面优化的业务价值非常大。


左右滑动查看外投软广的C端页面


目前外投模板都是采用 SSR 去做服务端渲染的,但是 SSR 渲染的整个流程如下图所示:

SSR 把数据拉取放到了服务端,因为离数据源比较近,数据拉取的速度会快一点。但这也不是完全没有副作用,因为需要在服务端等待数据就绪,TTFB(Time to First Byte) 相比 CSR 会长一点。

SSR 只是给我们准备好了初始的数据和 HTML,实际上和 CSR 一样,我们还是需要加载完整的客户端程序,然后在浏览器端重新渲染一遍(更专业的说是 Hydration  水合/注水),才能让 DOM 有交互能力。

也就是说,FCP(First Contentful Paint) 相比 CSR 提前了,但是 TTI(Time to Interactive) 并没有太多差别。只是用户可以更快地看到内容了。

我们生成环境 SSR  生成的 HTML 资源会被 CDN 缓存,也就是说当 CDN 有缓存的,上述图片中的流程是不会再走一遍的。

为什么选择 SSG 作为得物外投渲染的技术方案?

通过分析了目前得物外投的 C 端页面,有下列几点:

  • 重玩法和体验,而且本身就是投在短视频平台上的广告,大部分都是拉新,所以对于站外新用户可能就点击一次,后续再也不会点了,所以优化第一次的秒开体验还是很有必要的。

  • 运营对于模板配置的改动频率较低,不是高频操作。如果模板是高频改动,那么 SSG 的技术方案可能就不是特别适合。因为模板需要一直重新构建,这时候不如采用 SSR,保持新鲜度。

  • 外投页面对于接口请求的依赖十分弱。

根据这三点判断外投软广十分符合 SSG 的技术体系,可以节约第一次服务端拉取数据的时间,其实这部分的时间,反正也是生成 HTML,返回给客户端。其实在运营投出广告之前,模板已经构建完成了,而在构建的时候就直接生成好了所谓的静态外投模板页面。这种方式其实就做 SSG (静态生成)。

看下面这张图或许你就能明白 SSG 和 SSR 的区别:

从图中能明显看出来 HTML 是在构建的时候生成的,而不是在访问这个 HTML,服务器在动态生成。这就是两者之间的差距。所以对于第一次访问的,可以看下面这张图:

SSG 等待服务响应的时间是明显低于 SSR 的。看下面视频就可以明显感觉到差距。



SSG 和 SPA 预渲染骨架的区别在哪里?

SPA 预渲染骨架:是指在服务端对 CSR 页面进行预先渲染,在页面启动时直接返回已经渲染好的 HTML 文件给客户端,避免了客户端一开始加载页面就要进行 JavaScript 的解析和运行,这样能够提升页面的访问速度和用户体验。预渲染技术使用工具(如 Puppeteer)对 CSR 网页进行自动化爬取、渲染、生成 HTML 文件的操作。

从生成的内容来说都是 HTML,这一点没什么区别。但是从投放的业务来说,我们要静态化的页面较多,所以我们需要一个专门的服务帮我们根据模板 ID 生成各种静态 HTML。

在静态资源更新上,假设这么一个场景,线上预渲染的 HTML 有问题我们需要更新,如果 1-2 个页面没啥问题,我们可以重新构建一下,更新 HTML 缓存,如果有几千或者是上万个,SPA 预渲染估计也不太适合。而且频繁的发版、构建对于线上的稳定性也是一个不小的挑战。

在增量更新上,SPA 预渲染骨架是没法支持的,目前社区的一些插件实现原理都类似,都是根据特定 SPA 指定的路由通过 WebPack 或者是 Vite 去进行预构建然后生成对应的 HTML,还需要在nginx上配置特定路由转发, 不然走index.html 不够纯粹,这一点也 pass 掉。

基于上面的一些原因,选择了 Next 去做 SSG 框架支持。生态完善又可以持续更新,可以简化一部分开发工作。

那么 SSG 就没有什么缺点吗?

优点的话不用多说,相比 SSR,因为不需要服务端运行时进行数据拉取,TTFB/FCP 等都会提前。

在实际过程中还遇到下面几个问题:

  • 针对模板更新的时候,如何去进行静态页面缓存更新?

  • 对于没有静态生成的页面,我们还能访问吗?

针对上述问题我们在技术方案里引入 ISR (增量静态生成)。

如下图所示:

ISR 是 SSG 的升级版,解决 SSG 内容变更的问题。

ISR 依旧会在构建时预渲染页面,但是这里多出了一个服务端运行时,运行时会按照一定的过期/刷新策略(通常会使用 stale-while-revalidate )来重新生成页面。

关于这部分内容,本篇文章不做详细介绍,只是引出关键概念,看到这里如果觉得写的不错的,可以给作者点个赞哟!


三

页面性能优化


其实光做 SSG 在页面体感上其实已经可以达到比之前的快了,但是光做这个是不够的。因为在 CDN 有缓存的情况下,SSG 和 SSR 的 HTML 都缓存在 CDN 上,这里的话其实两者区别不是特别大。

关于性能优化市面上有很多文章,这里不做过多叙述,本篇文章只针对得物外投广告业务,探索符合外投广告优化的性能最佳实践。


自定义文档行为


自定义文档行为,主要是修改 next.js 默认的文档行为,只需要在页面新增一个_document.tsx  _app.tsx 如下图所示:


CSS 内联


这里我们做的第一个行为就是 CSS 内联  通过继承 Head 组件,重对应的子方法。


































class InlineStylesHead extends Head {  getScripts(files: any) {    return null  }
 getPolyfillScripts(): JSX.Element[] {    return this.context.buildManifest.polyfillFiles.map((file) => {      return <script defer={true} noModule src={`${ASSETPREFIX[SENV]}/_next/${encodeURI(file)}`}></script>    })  }
 getCssLinks(files: any) {    const cssFiles = files.allFiles.filter((f) => f.endsWith('.css'))    return this.__getInlineStyles(cssFiles)  }  __getInlineStyles(files: string[]) {    const { assetPrefix } = this.context    if (!files || files.length === 0) return null    return files      .filter((file) => /\.css$/.test(file))      .map((file) => (        <style          key={file}          data-href={`${assetPrefix}/_next/${file}`}          dangerouslySetInnerHTML={{            __html: readFileSync(join(process.cwd(), '.next', file), 'utf-8'),          }}        />      ))  }}#因为doucment 文件是在 node 侧执行的, 所以当生成静态的html 的时候, 这个时候静态资源 webpack 打包已经生成好了, 我可以通过读写文件名,找到对应的文件内容。如图所示:build 的时候,这里的值会内联首屏需要的css,不在首屏的css 不会内联进去。

背景大图优化


这里的话由于背景图是默认静态生成的时候已经是内联了,这时候是没有 Webp 的,如果通过 React State 去控制背景图的 Webp 展示的话,会导致页面闪一下,用户体验不好,但我们又想优化背景图的体积,于是我们在自定义 Doucument 里面加了一个原生 Dom 操作如图所示,支持 Webp 的优化成 Webp,不支持的优化成 jpeg 格式。
















 <script          dangerouslySetInnerHTML={{            __html: `          var el = document.getElementById('nineRoot');          if(el) {            if(window.isSupportWebp) {              el.style.backgroundImage = 'url(${global.nineImg || defaultUrl}?x-oss-process=image/format,webp)';            } else {              el.style.backgroundImage = 'url(${global.nineImg || defaultUrl}?x-oss-process=image/format,jpeg)';            }          }        `,          }}        />


优化过后的体积大小对比:

足足差了 100KB 左右。


打包优化

SWC


Nezha 是采用 Next 12 版本系列,我们新的轻量级落地页采用 Next 13,有一个很重要的特点使用了 SWC 而不是 Babel。

我们选择使用 SWC 进行开发有以下几个原因:

  • 可扩展性:使用 SWC 可以作为 Next.js 中的 Crate,而不必分叉库或绕过设计限制。

  • 性能:通过切换到 SWC,我们在 Next.js 中实现了大约 3 倍更快的快速刷新和 5 倍更快的构建速度,还有更多的优化空间正在进行中。

  • WebAssembly:Rust 对 WASM 的支持对于支持所有可能的平台并将 Next.js 开发带到任何地方至关重要。

  • 社区:Rust 的社区和生态系统是惊人的,而且还在不断壮大。

  • 这里还有一个很重要的原因就是后续我们会新增 ISR,也就是服务端不存在的静态模板 ID 资源的时候,我们通过接口触发 Node 层进行构建,构建的速度越快,用户等待的时间也越少。所以更换语法转换还是有很有必要的,对于后期 ISR 是非常有必要的。

具体了解 SWC 可以看这个网站:https://swc.rs/


组件库按需加载


Babel 情况下的按需构建我们都是通过安装 babel-plugin-import 通过如下配置:

 [{"libraryName": "antd","libraryDirectory": "lib",   // default: lib"style": true},{"libraryName": "antd-mobile"},]

但是新版本已经内置了这种能力。

可以在 next.config.js 中这样配置,具体的详细配置可以看这个链接:https://nextjs.org/docs/architecture/nextjs-compiler#modularize-imports








 transpilePackages: ['antd-mobile'],  modularizeImports: {    'antd-mobile': {      transform: 'antd-mobile/es/{{ lowerCase member}}',    },  },

首屏资源动态化


对于首屏资源的优化其实是很有必要的,由此提出下面 2 个策略:
  • 抽取外投模板通用的组件,比如投放 Banner 五要素、唤端组件。

  • 不在首屏资源的组件,延迟加载。

抽取了动态组件,Webpack 打包的时候,对于动态组件相关的资源会打包到一起,不会打包到主 JS 里面,可以很好地提高 CDN 缓存利用率,防止因为一些简单的改动,影响整个 JS 的 Hash。

这里用到了 next/dynamic ,具体的链接可以看这个: https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading#nextdynamic













import dynamic from 'next/dynamic'
const DynamicDetainmentPopup = dynamic(() => import('./detainmentPopup'), {  loading: () => <div></div>,  ssr: false,})const DynamicToufangBanner = dynamic(() => import('./toufangBanner'), {  loading: () => <div></div>,  ssr: false,})#next/dynamic 是 React.lazy() 和 Suspense 的组合, 其实就是组件懒加载, ssr 为true 表示是服务端渲染  为false 的话表示是客户端渲染, 打包后的路径区别在于  服务端渲染的话也就是ssr 为true 打包生成的chunks 会在 server  目录下,客户端渲染的 打包后的chunk 在 static 目录下  。



但是无论是那种方式,我们线上最终访问的资源都是在 OSS 上的。

所以我们需要在 Webpack 配置下 PublicPath:

     config.output.publicPath = CDN_MAP[process.env.APP_ENV] ||  '//h5static.dewu.com/h5-launch-ssr/'

通过上面这个步骤首屏资源减少几十 KB。


图片资源优化


重写 Postcss-Webp 插件,重写的背景原因是由于之前的 OSS 域名改造,静态资源都会更新成 H5static.dewucdn.com。

造成部分图片资源没有应用到 Webp 所以这里重新梳理了这块逻辑,重新封装了。

 "@dw/postcss-webp": "0.1.2"


Http2.0下更优雅的拆包


https://webpack.docschina.org/plugins/split-chunks-plugin/

  • 针对 node_modules 资源:

    • 可以将 node_modules 模块打包成单独文件(通过 cacheGroups 实现),防止业务代码的变更影响 NPM 包缓存,同时建议通过 maxSize 设定阈值,防止 Vendor 包体过大。

    • 由于我们的资源有专门的静态加速域名且支持 Http2.0,这里将每一个 NPM 包都打包成单独文件。

  • 针对业务代码:

    • 设置 common 分组,通过 minChunks 配置项将使用率较高的资源合并为 Common 资源。

    • 首屏用不上的代码,尽量以异步方式引入,这里上面 Dynamic 已经做了拆包。

    • 设置 optimization.runtimeChunk 为 True,将运行时代码拆分为独立资源,这里 Next 也已经内置了。

所以我们的一个优化策略就是将 @du @growth 的 NPM 包进行拆分。




























  config.optimization.splitChunks.cacheGroups.dw =  {          name: `@dw`,          test: testPackageName('@dw'),          priority: 500,          enforce: true,         }        config.optimization.splitChunks.cacheGroups.du =  {          name: `@du`,          test: testPackageName('@du'),          priority: 500,          enforce: true,         }        config.optimization.splitChunks.cacheGroups.growth =  {          name: `@growth`,          test: testPackageName('@growth'),          priority: 400,          enforce: true,         }
       config.optimization.splitChunks.cacheGroups.className =  {          name: `classnames`,          test: testPackageName('classnames'),          priority: 400,          enforce: true,        }      }

第三方包体积优化


  • 神策埋点优化:神策实例包体积重复加载优化,减少神策包加载体积 25KB+。

由于唤端 App 和落地页都会进行埋点上报,原有的逻辑是 Call-App 内部也会再去加载神策埋点 JS,但是在外投落地页展示的时候其实已经加载过,导致神策包体积重复加载。

代码如下:

// 如果神策实例 已经加载了 call-app 这里不需要加载  if (window.sensorsDataAnalytic201505) {    sensors = window.sensorsDataAnalytic201505;    return;  }
  • 统一请求库:统一去掉 Axios 统一用 Fetch 去做请求,减少了 JS 体积 20KB+。保持项目请求库技术栈统一,这里不需要担心 Fetch 的兼容性问题,Next 13 默认做了 Fecth 的 JS 垫片。


四

上线前的准备


新版落地页上线前,我们做了一轮产品内测,一个是开发视角,以表单问卷的方式进行了做了内测问题收集。

一共收集了如下几个问题:

其中运营侧有一个很强烈的需求,就是强烈要求新版落地转盘速度变快,觉得用户的耐心值比较低,本身就是广告了,点进去运转的慢的话,用户会直接流失。


五

得物页面级别的灰度AB


轻量版本的落地页做出来了,贸然去投外投广告本身是不合适的。所以这里针对同一个外投软广链接,去做 AB 测试,排除用户的群体、包括投放的策略、以及达人的量级等等这些外在因素的影响,去分析 AB 情况下的新版和老版的性能指标。我们验证了新的轻量落地页,无论是在业务还是性能上都有显著提升,那么我们肯定需要大规模灰度,对于线上已经投放的广告,如何进行无缝灰度?针对这一点,运营对我们前端提出的要求很简单,由于线上已经创建了大量的广告计划,运营不可能手动一个个去改链接,工作量很大。所以核心点就是在不改线上链接的前提下,线上用户访问的是我们的新的落地页资源。


六

全量的灰度收益


通过 ES 灰度验证后,实验效果显著。推全后优化软广外投页面九宫格模板日均 xx 万 PV (占据整个外投的 xx% 的流量),提高外投模板性能 40pp 左右,提高广告转化率 2pp 左右。


性能前后对比


推全之前的外投性能 FCP 平均在 30%,推全之后稳定在 70%,提高了 40pp 左右。


业务前后对比


推全之前近一个月的数据平均值,九宫格的 CVR 在 50+,辅助参考指标:真实唤端成功率平均值在 19+。

推全之后的数据:CVR 和真实唤端成功率均显著正向,预计提高 CVR 转换 2pp 左右。


七

灰度方案


在谈技术方案之前先普及以下概念。


条件源站


默认的话 CDN 回源只有一个源站,比如 CND-A.com 当回源的时候会直接回源到 A.com。如果你想 CDN-A.com 要回源到 B.com ,那就需要在 CDN 域名管理的 控制配置如下图所示:

结合具体的规则引擎配置比如我们配置的是参数中含有 _launch_ 就会回源到B.dewu.com。


八

ES


目前条件源站配置好了,线上的所有 CDN-A.com 的链接如何自动的大批量都加上 _launch_ 参数呢?那就需要 ES 登场了。

边缘脚本(EdgeScript,简称 ES)。CDN 节点网关会根据你在控制台上设置的标准配置、边缘脚本规则对请求进行处理。ES 规则的执行位置可以是请求处理开始或者请求处理结尾。所以 ES 的执行配置,可以在请求前,也可以在请求后。这里的配置顺讯会决定条件源站和 ES 的应用顺讯。如下图所示:

结合上面的图,整体的流程已经很明显了,客户端请求先回到 ES ,ES 里面我们会写下灰度匹配脚本。

代码这里就不放了,感兴趣的可以去阿里云 ES 脚本去学习一下。

大体的意思就是匹配 URL 含有某些会场 ID,匹配上了加上 &_launch_=1&esab=1 这两个参数。

然后在经过条件源站,就可以回源到我们新的轻量落地页了。这里解释一下参数上加 Esab 其实主要是一个锁,防止请求在 ES 死循环。

rand_hit(20) 是 ES 语法提供的随机函数,可以进行站外简单的 AB 分流,也就是线上的链接是有概率进入 ES 并且触发条件源站回源。 这里做的比较简单,其实还可以配上 Cooike,达到命中过灰度了,就一直是灰度的链接。站外软广这里不存在登录态,和产品们讨论,问题不是很大。


架构图


普及前面的概念,直接捋一下灰度的流程图如下:

流程图上有一块兜底的考虑:万一有配错的会场,回源到 B.com ,我们会进行反向代理,会直接代理到 A.com 确保会场的正确显示。


九

灰度节奏


  • 白名单生产测试,ES 脚本匹配 ID 符合上面 7 个模板 ID,然后再加一个白名单测试参数,先真实广告测试唤端对应的场景是否正确。

  • 白名单测试完毕之后,去掉白名单测试参数之后 ES 分设备进行灰度,先 iOS 后再安卓(iOS 的流量比安卓的小 )还是先预发验证,预发验证结束在应用生产正式的 ES。

  • 这里灰度的会场 ID 还是逐步灰度观察数据,每一次灰度的会场 ID 都是先 iOS 再安卓进行测试,避免小流量发现不了问题。


十

灰度时间安排


测试完毕后:

  • DAY1 上午 10 点 iOS 灰度百分比 50%。DAY2 早上灰度当前模板,安卓 10点。

  • DAY4 上午 10 点 iOS 灰度 10% ,观察 2 小时有问题随时切走。 DAY4 晚上 9 点灰度 30% 。DAY5 上午 10 点灰度安卓 10%,晚上 9 点灰度 30%。

  • Day 6 上午 10 点 iOS 灰度 10%,观察 2 小时有问题随时切走。DAY6 晚上9点灰度30%。DAY7 上午 10 点灰度安卓 10%,晚上 9 点 灰度安卓 30%。

推全的灰度安排就是上面所有会场 ID 不分渠道进行灰度按照 10% 20% 50%  65% 80% 100% 逐渐切入。


十一

稳定性


敬畏生产。对于一个比较大的技术项目,稳定性的监控十分重要。我们业务的核心指标:唤端成功率,拉新激活率,JS异常。

我们做了下面这几件事:

  • SLS 监控告警监控实时唤端成功率,有没有出现同比大幅度下跌。

  • 代理失败兜底监控。

  • 拉新大盘监控。

  • Error 监控报错。



来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70027824/viewspace-3003433/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2023-03-16

  • 博文量
    358
  • 访问量
    290108