我们如何看待服务器渲染及其结果

大家好! 在这一年中,我们切换到React并考虑如何确保我们的用户不等待客户端模板化,而是尽快查看页面。 为此,我们决定进行服务器端渲染(SSR-服务器端渲染)并优化SEO,因为并非所有搜索引擎都能够执行JS,并且那些能够花费执行时间的搜索引擎,并且每个站点的爬网时间都受到限制。



让我提醒您,服务器渲染是在服务器端执行JavaScript代码,以便为客户端提供就绪的HTML。 这会影响用户感知的性能,尤其是在速度较慢的计算机和速度较慢的Internet上。 无需等待,直到下载,解析和执行JS。 浏览器只能立即呈现HTML,而无需等待JSa,用户已经可以读取内容。
因此,减少了被动等待阶段。 渲染后,浏览器将只需要浏览完成的DOM,检查是否与渲染的内容匹配
在客户端上,并添加事件监听器。 这个过程称为水合 。 如果在水合作用过程中,服务器中的内容与浏览器生成的内容之间存在差异,我们将在控制台中收到警告,并在客户端上获得一个额外的渲染器。 事实并非如此,必须确保服务器和客户端渲染的结果匹配。 如果它们不一致,则应将其视为错误,因为这会否定服务器呈现的优势。 如果任何元素应该发散,请向其添加suppressHydrationWarning={true}


另外,有一个警告:服务器上没有window 。 访问它的代码必须在服务器端未调用的生命周期方法中执行。 也就是说,您不能在UNSAFE_componentWillMount()中使用window ,对于钩子,不能使用uselayouteffect


实际上,服务器端渲染过程归结为从后端获取initialState,通过renderToString()运行它,在输出中拾取完成的initialState和HTML,然后将其发送给客户端。


在hh.ru中,仅在python的api网关中允许从客户端JS进行加息。 这是为了安全和负载平衡。 Python已经转到必要的数据后端,准备数据并将其提供给浏览器。 Node.js仅用于服务器渲染。 因此,在准备好数据之后,python需要额外的行程到节点,等待结果并将响应发送给客户端。


首先,您必须选择一个可以使用HTTP的服务器。 我们在koa停了下来。 喜欢await的现代语法。 模块化是轻量级的中间件,必要时可以单独安装或轻松编写。 服务器本身轻巧, 快速 。 是的,由koa由与他们共同编写的同一开发团队编写,他们的经验着迷。


在了解了如何推广我们的服务之后,我们在KOA上编写了最简单的代码,该代码能够提供200,然后将其上传到产品。 它看起来像这样:


 const Koa = require('koa'); const app = new Koa(); const SERVER_PORT = 9400; app.use(async (ctx) => { ctx.body = 'Hello World'; }); app.listen(SERVER_PORT); 

在hh.ru中,所有服务都位于docker容器中。 在第一个发行版之前,您需要编写ansible剧本,借助该剧本,该服务可以在生产环境和测试台上推广。 每个开发人员和测试人员都有自己的测试环境,这与prod最相似。 我们花费了大部分时间和精力来编写剧本。 发生这种情况是因为有两个前端渲染器执行了此操作,这是hh.ru中节点上的第一个服务。 我们必须弄清楚如何将服务切换到开发模式,并与进行渲染的服务并行进行。 将文件传递到容器。 启动裸服务器,以便docker容器启动而无需等待构建。 使用服务器构建和重建服务器以及服务。 确定我们需要多少RAM。


在开发模式下,当更改最终版本中包含的文件时,它们提供了自动重建和随后重新启动服务的可能性。 节点需要重新启动以加载可执行代码。 Webpack监视更改和构建 。 需要Webpack才能将ESM转换为通用CommonJS。 为了重新启动,他们采用了nodemon ,它负责收集收集的文件。


然后,我们学习了路由服务器。 为了实现适当的平衡,您需要知道哪些服务器实例处于活动状态。 为了检查这一点,可操作的心跳每几秒钟进入一次/status ,并期望收到200次响应。 如果服务器的响应次数不超过配置中指定的次数,则将其从平衡中删除。 原来这是一个简单的任务,几行代码并准备好路由:


 export default async function(ctx, next) { if (routeMap[ctx.request.path]) { routeMap[ctx.request.path](ctx); } else { ctx.throw(NOT_FOUND, getStatusText(NOT_FOUND)); } next(); } 

我们在正确的网址上回答200:


 export default (ctx) => { ctx.status = 200; ctx.body = '200'; }; 

之后,我们制作了一个原始服务器,该服务器以<script>返回状态并准备好HTML。


有必要控制服务器的工作方式。 为此,您需要加快日志记录和监视速度。 日志不是用JSON编写的,而是为了支持我们其他服务(主要是Java)的日志格式。 Log4js根据基准测试选择的-它快速,易于配置并以我们所需的格式编写。 需要一种通用的日志格式来简化监视支持-无需编写额外的常规文件来解析日志。 除了日志,我们仍然在sentry中写入错误。 我不会给出记录器的代码,这很简单,基本上有设置。


然后,有必要提供正常的关机-当服务器生病或发行发布时,服务器不再接受任何传入的连接,而是执行所有挂在其上的请求。 节点有许多现成的解决方案。 他们采取了http-graceful-shutdown ,所有要做的就是包装gracefulShutdown(app.listen(SERVER_PORT))调用gracefulShutdown(app.listen(SERVER_PORT))


至此,我们得到了可用于生产的解决方案。 为了检查其工作原理,他们在一页上为5%的用户打开了服务器渲染。 我们查看了这些指标,意识到它们显着改善了手机的FMP ,对于台式机,其价值并未改变。 他们开始测试性能,发现其中一台服务器可容纳约20 RPS(对于Javists而言,这一事实非常可笑)。 找出原因的原因:


  • 原来的主要问题之一是,它们是在没有NODE_ENV = production的情况下构建的(我们设置了服务器构建所需的ENV)。 在这种情况下,反应产生了非生产组装,其运行速度降低了约30%。


  • 我们将节点的版本从8提高到10,又获得了20-25%的性能。


  • 我们最后一次剩下的是在多个内核上启动一个节点。 我们怀疑这是非常困难的,但是在这里,一切也变得平淡无奇。 该节点具有内置机制-cluster 。 它使您可以运行所需数量的独立进程,包括将其分散任务的主进程。



 if (cluster.isMaster) { cluster.on('exit', (worker, exitCode) => { if (exitCode !== SUCCESS) { cluster.fork(); } }); for (let i = 0; i < serverConfig.cpuCores; i++) { cluster.fork(); } } else { runApp(); } 

在此代码中,主进程启动,进程根据为服务器分配的CPU数量启动。 如果子进程之一崩溃-退出代码不为0 (我们自己关闭了服务器),则主进程将重新启动它。
而且性能提高了大约为服务器分配的CPU数量。


如我上面所述,大部分时间都花在编写原始剧本上-大约3周。 编写整个SSR大约花了2周的时间,而大约一个月后,我们才慢慢想到它。 所有这些都是由2条战线完成的,没有Node js的企业经验。 不要害怕做SSR,最重要的是-不要忘记指定NODE_ENV=production ,这没有什么复杂的。 用户和SEO将感谢您。

Source: https://habr.com/ru/post/zh-CN445816/


All Articles