为了解决首屏白屏和搜索引擎抓不到正文的问题,团队把整站从客户端渲染统一改成了服务端渲染。上线头两周效果很明显:首屏时间下来了,抓取方拿到的初始 HTML 里终于有内容了。但从第三周开始,渲染进程的内存占用一路往上走,每天要人工重启一次才安心;到了业务高峰期,偶发的进程崩溃会直接让整站返回错误页,重启那几十秒里的请求全部失败。首屏确实快了,稳定性却变差了。
复盘时最容易得出的结论是"服务端渲染这条路走不通"或者"该换一个框架"。把监控数据摊开看会发现,内存增长主要和三个量相关:同时在渲染的页面数、单页取回的数据体积、以及进程存活期间有没有被正确回收。这三项和具体用哪个框架关系不大,和"有多少页面在走服务端渲染"关系很大。
真正的毛病在改造的前提上。这次改造是把全站所有页面统一换成了同一种渲染方式,其中包含了大量所有人看到的内容都一模一样的页面。一个内容对所有人相同的页面,每次请求都在服务端重新生产一遍完整 HTML,本质上是在用常驻的 CPU 和内存重复制造同一份产物。而真正需要每次重新计算的,可能只占全站页面的一小部分——登录态、会员价、推荐位、实时库存、账户余额。
把这两种性质完全不同的页面用同一种方式处理,才会同时出现"首屏好了"和"内存爆了"这两个看似矛盾的结果。所以本文要讨论的问题不是"服务端渲染好还是客户端渲染好",这个问法本身没有答案,因为页面与页面之间的差别,比渲染模式之间的差别大得多。
先说明:以下只讲形态与代价,不做任何产品或厂商推荐。不同形态在工程上的实现千差万别,但代价结构是稳定的。
第一种,客户端渲染。服务端只返回一个 HTML 骨架和一段脚本,真正的 DOM 由浏览器在本地构建,数据通过后续的接口请求取回。它的好处是对源站的压力结构最简单:源站只出静态文件和接口,不需要为每个页面请求付出渲染算力,部署和发布也最轻。代价有两层:一是首屏要等脚本下载、执行、再发一次或多次数据请求,链路上的每一跳都算进首屏时间,且这段耗时高度依赖客户端设备的算力与网络;二是抓取方如果只取初始 HTML 而不执行脚本,拿到的就是一个空壳,正文、标题、结构化数据都不在里面。
第二种,源站服务端渲染。每个请求到达源站时,由常驻的渲染进程现场生成完整 HTML 再返回。首屏与抓取是它的强项:浏览器拿到的第一份响应里就有内容,不需要再等一轮数据请求。代价集中在源站这一侧——渲染进程必须常驻,内存占用随"同时在渲染的页面数乘以单页数据体积"增长;CPU 在高峰期吃紧;更关键的是,渲染进程成了整条链路上的单点,进程崩溃会直接表现为全站报错,而不是某一个页面报错。这也解释了开篇那个现象:把抓取友好的收益拿到的同时,把稳定性风险也一并接了过来。
第三种,预渲染与构建期生成。页面在构建阶段就生成为静态文件,请求到达时只是读取文件。这是回源压力最小的一种形态,进程崩溃的风险也基本消失,因为没有"现场渲染"这个环节。代价是内容更新必须重新走一次构建:改一个错别字也要触发构建与发布;页面数量大时,构建时间会随页面数线性甚至更差地增长,产物占用的存储空间也随之增长,历史版本的保留策略会成为另一个要管理的问题。此外,内容更新到线上生效之间存在一个由构建时长决定的延迟窗口。
第四种,边缘渲染。渲染发生在离用户更近的节点上,缩短了用户到渲染位置之间的往返时间。它并没有消除渲染本身的算力开销,只是把开销从源站搬到了别处;真正被缩短的是网络往返,不是渲染耗时。它引入的新问题是边缘与源站之间的状态与缓存一致性:数据在哪一侧取、版本以谁为准、失效指令如何到达所有节点。还有一个常见误判需要点明——边缘渲染不等于回源次数减少,回源次数取决于缓存命中,如果每个请求都要到源站取数据再渲染,往返反而可能变长。
这四种形态不是互斥的,也不是只能选一个。一个站点完全可以同时存在四种形态,分别服务不同性质的页面。真正要决定的不是"选哪一种",而是"哪一种页面放哪一层"。
分区的第一个变量是"同一份内容,对谁是一样的"。可以粗略分三档:对所有人一样、对某个分组一样、对每个用户都不一样。
对所有人一样的内容,理论上只需要生产一次,之后所有请求共用。首页的栏目结构、内容详情页的正文、帮助文档、关于我们,都属于这一类。这类内容的可共享比例接近百分之百,把它放在每次请求都重新渲染的位置上,是最典型的一种浪费。
对某个分组一样的内容,按地区、语言、会员等级、渠道来源、设备类型等维度分桶。同一个桶内的用户可以共用一份产物,桶的数量决定了要维护多少份产物。这里要小心维度相乘:地区乘以语言乘以设备类型,三个维度各取几档,变体数量就是乘积关系,很快就不是一个可控的数字了。
对每个用户都不一样的内容,是最难共享的一类。登录态、昵称、购物车条数、个性化推荐位、账户余额、订单状态,这些内容的产物只对一个人有效。如果这类内容被放进主文档的缓存键里,那么这份缓存实际上退化为"只服务一个请求",命中率会掉到接近零,而内存与存储开销却一点没少。
关键判断在这里:一个页面通常不是纯粹的某一档,而是三档的混合。一个内容详情页,正文对所有人一样,价格可能按会员等级分档,右侧的推荐位对每个用户不同,"已登录"这个标记又是一个全局开关。把页面整体判为"个性化页面"从而整体排除在缓存之外,是分区决策里最常见的一个错误。正确做法是把页面拆开看,分别判断每一块属于哪一档。
由此得到一条可以直接用的结论:个性化程度越高,能被共享缓存的比例越低,把渲染放在离源站越远、越靠近用户的层就越有必要共享;反之,个性化程度低的内容,越应该推到边缘或静态化。这里说的"推到边缘",指的是让产物在离用户更近的位置被命中,而不是把渲染本身搬过去。
第二个变量是内容的变化节奏。按从快到慢排,大致有分钟级、小时级、天级、以及"只有发布才变"四档。
更新频率直接决定缓存有效期能设多长,但这两者不是简单的等号。中间还夹着一个业务判断:这个内容陈旧多久是可以接受的。库存数量陈旧五分钟可能引发超卖;一篇文章的错别字陈旧一小时完全无所谓;一个活动页的价格标签陈旧一分钟就可能引发投诉。也就是说,有效期上限由业务陈旧容忍度决定,而不是由技术能力决定。
分钟级变化的内容,缓存有效期只能设在分钟量级,这意味着缓存命中的窗口很短,大部分请求仍然会走到渲染或回源。对于这类内容,把渲染放在边缘并不会带来多少收益,因为瓶颈不在网络往返,而在内容本身不许被缓存。此时更应该考虑的是缩小变化范围——比如把"剩余库存"这一小块单独异步取回,而让页面的其余部分获得更长的有效期。
天级或发布才变的内容,是预渲染与长有效期缓存的主要适用对象。这类内容的产物可以在构建期一次性生成,也可以用很长的有效期配合主动失效来管理。它占用的构建时间与存储空间会比较大,但换来的回源削减也最明显。
还有一个容易被忽略的情形:变化频率不均匀。一个商品详情页平时一天改一次,但在活动开始那一分钟会集中改一大批;一个内容页平时不动,但编辑可能在某个下午连续改十几版。按平均频率设有效期会在突变时失效不及时,按最快频率设又会让平时的大量请求白白回源。这类不均匀的内容更适合用主动失效而不是单纯靠时间过期。
第三个变量是把前两个变量落到具体页面上:这个页面的产物里,有多大比例是可以被共享缓存的。
实际经验是,绝大多数页面真正个性化的部分占比很小。一个内容或商品详情页,个性化的可能只是顶部的登录态条、右侧的推荐位、底部的一段"猜你喜欢",以及价格或库存这类的动态字段。页面的主体——标题、正文、图片、参数表、规格、评价列表——对所有人都是一样的。也就是说,一个看起来"必须每次渲染"的页面,可能有百分之八九十的内容是可以共享的。
既然如此,做法就是把这一小块个性化内容单独取回,让其余部分进入缓存。具体有几种形态:主文档缓存后在客户端用一个异步请求补上个性化块;或者在缓存层做拼接,把个性化片段作为不可缓存的小块单独注入;或者干脆把个性化块做成延迟渲染——先出可缓存的主体,个性化部分在首屏之后再填充。
这个做法的收益是显著的:主文档的缓存键里不再包含用户维度,命中率不再被登录态拖垮;回源流量大幅下降;源站的渲染进程只需要处理真正需要渲染的那部分请求。代价是页面从"一次拿到全部"变成"至少两次请求",首屏时间可能略有变化,且个性化块的加载失败需要有明确的兜底表现——比如显示默认值或者隐藏该区域,而不是留一块空白或者报错。
这里有一个需要提前想清楚的取舍:个性化块如果是首屏可见的关键信息(比如价格、库存状态),把它延后填充会让用户在首屏看到"暂无"再跳变,体验反而更差。这种情况下更合适的做法是把个性化块缩小到只剩真正因人而异的字段,把可以分桶的部分(比如按会员等级分档的价格)归入分组档,用少量变体换取可缓存性。
判断可缓存比例还有一个实用方法:把页面的数据依赖列出来,逐个标注"这个数据是全局的、分组的、还是单人的"。如果单人数据只有一两个,就具备拆分的价值;如果整页数据几乎都依赖用户身份,那这个页面本来就该放在不可缓存的那一层,不必强拆。
缓存是否命中,取决于缓存键怎么构造。常见的键维度包括:请求路径、查询参数、设备类型、地区、语言、登录态标记。构造键的基本原则是"产物会因之而变的东西,必须进键;产物不会因之而变的东西,绝不能进键"。
但实际工程里,出问题的大多是后者——把不该进键的东西放了进去。键里每多一个维度,可能的键组合数就乘一次,命中率就下降一档。比如原本只按路径缓存,一个页面只有一个变体;加上设备类型两档,变体变成两个;再加上语言三档,变成六个;再加上地区若干档,变体数量继续翻。同样的请求量被摊到更多变体上,每个变体的请求密度下降,命中率自然走低,而缓存占用的存储却在上升。
其中"登录态"是最容易毁掉命中率的维度。只要把登录态放进键,页面就至少分裂成"已登录"和"未登录"两个变体;更糟的情况是把用户标识本身放进键,那么每个用户一个变体,缓存实际上完全失效,却仍然在消耗存储与写入开销。这正是上一节强调把登录态条单独异步取回的原因——它让主文档的键里彻底不需要出现用户维度。
查询参数是另一个高频污染源。同一个页面可能带上来源标记、推广参数、随机时间戳、AB 分桶参数等大量查询串。如果不做规范化,每一个不同的参数组合都会生成一个独立缓存项,命中率会被这些无意义的参数打散。正确做法是明确一份参与键计算的白名单,其余参数一律忽略;确需参与的参数要先做排序与归一化处理。
地区与语言这两个维度通常无法省略,因为产物确实会变。但要注意它们是分组维度而不是单人维度,只要档位控制在合理范围,对命中率的影响是可接受的。真正要警惕的是把连续值当成键维度——比如把精确到城市的定位结果、把分桶编号很细的实验分组放进键,这会让变体数量失控。
缓存里的内容总要被更新,更新的方式有三种,代价结构差别很大。
按时间过期,也就是 TTL。最简单,不需要维护任何额外的元数据和推送链路,只要给每个缓存项设一个存活时长。代价是一定存在陈旧窗口:内容改了之后,最长要等一个 TTL 才能生效。这个窗口的长度就是业务陈旧容忍度的上限。TTL 还有一个附带问题——大量缓存项如果是在同一时刻写入的,也会在同一时刻集体过期,形成周期性的回源尖峰,需要靠加随机抖动来打散。
按标签或路径主动失效。内容变更时,由变更方发出失效指令,精确地把受影响的缓存项标记为无效。这种方式可以把陈旧窗口压到很短,也可以设很长的 TTL 而不担心陈旧——因为更新走的是主动通道而不是等过期。代价是必须维护一套标签体系:每条内容要挂上它影响哪些页面、哪些列表、哪些聚合,这些关系要随业务变化同步更新,一旦漏挂就会出现"改了但没生效"的顽固问题。此外还要建设失效指令的推送链路,并考虑指令丢失时的重试与补偿。
按版本重建。内容变更时触发一次构建,生成新版本产物并整体切换。这是最彻底的方式,不存在"部分生效"的问题,也天然支持快速回滚——切回上一个版本即可。代价全部压在构建侧:构建是 CPU 密集任务,页面数量大时耗时可观,且构建期间会占用算力;产物需要存储空间与版本保留策略;频繁的小改动会频繁触发全量或大范围构建。
实际部署里通常是三者组合:主体内容用长 TTL 配合主动失效,保证平时不回源、改了能立刻生效;少数更新极慢的内容走构建期生成;无法挂标签的临时内容用短 TTL 兜底。选择哪一套,取决于内容变更能否被系统感知——如果内容是从数据库或内容管理系统里改的,通常可以挂上失效钩子;如果内容来自无法感知的外部流程,那就只能靠 TTL。
缓存系统最危险的时刻不是命中率低,而是某个热点键突然失效的那一瞬间。设想一个首页或一个活动页,平时几乎全部命中;当它因为过期或被主动失效而变为无效时,短时间内的全部请求会同时发现"没有缓存",然后同时向源站发起渲染请求。这一刻的回源量等于平时的全部请求量,源站的渲染进程会在几秒内被压满,表现为 CPU 打满、内存暴涨、进程开始崩溃。
更糟的是雪崩效应:源站变慢之后,缓存层等待回源的超时拉长,堆积的请求越来越多,源站恢复的时间被进一步推迟,形成正反馈。这类故障的表现特征是"平时非常稳,失效后突然全站不可用",很容易被误判为源站容量不足,从而走上无意义的扩容之路。
请求合并是第一道防线。同一个缓存键在回源未返回之前,后续到达的请求不应该各自发起一次回源,而应该挂起等待第一个回源的结果,然后共用它。这样无论瞬间到达多少请求,对源站也只有一次回源。实现上要保证合并的粒度就是键本身,并且要处理首个请求失败时的快速释放,避免所有挂起请求一起等到一个超时。
降级兜底是第二道防线。渲染失败时不应该返回错误页,而应该返回上一版本的产物——哪怕它已经过期,对用户来说一份稍旧的正确页面远好过一份错误页。这要求缓存层在失效时不是直接删除,而是标记为"陈旧但可用",并保留一段时间。如果连上一版本都没有,就返回一份静态兜底页,这份兜底页本身必须是极简且不依赖任何后端数据的,否则它也会跟着一起挂。
还有两个配套做法。提前异步刷新:对热点键,在它即将过期之前由缓存层主动发起一次后台回源,这样热点键几乎不会真正进入失效状态。容器与进程隔离:渲染进程要有明确的超时与熔断,单次渲染超过阈值就放弃并返回兜底,避免一个慢请求长期占用一个渲染槽位,把整个进程池堵死。
这里有一条明确的禁止项:不要让源站在没有请求合并的情况下直接面对缓存击穿。这是所有渲染位置选择里最不该省的一环,无论渲染放在源站还是边缘,只要存在缓存,就必须有这一层保护。
把渲染位置的选择翻译成服务器资源,主要落在三笔账上。以下只讲结构与取舍关系,不给具体配置数字,因为合理的取值高度依赖页面复杂度、数据量与实际请求分布,需要按自身情况实测后确定。
第一笔:渲染进程的资源账。渲染进程是常驻的,它的内存占用由两部分叠加——进程本身的固定开销,加上"同时在渲染的页面数乘以单页渲染时的数据体积"。这意味着内存不是随请求量线性增长,而是随并发渲染数增长:请求排队不会多吃内存,但同时开十个渲染上下文就会。CPU 则在高峰期吃紧,因为渲染是计算密集的,尤其是模板编译、数据序列化、以及大对象构建这些环节。
这笔账的取舍要点有三个。其一,必须限制并发渲染数上限,超过上限的请求要么排队要么降级,否则一次流量尖峰就能把内存吃穿。其二,必须多实例部署并具备故障自动拉起,因为渲染进程是单点风险,单实例的崩溃等于全站不可用。其三,必须设渲染超时与熔断,避免慢请求长期占用槽位。此外还要关注内存回收:如果进程长期运行而内存不回落,说明存在引用未释放的问题,这时加内存只是推迟崩溃时间,不是解决办法。
第二笔:缓存层与回源带宽的账。命中率直接决定回源流量与源站负载。命中率高时,源站只需要处理少量回源;命中率一旦下滑,回源量会不成比例地上升——因为回源请求本身还会触发渲染,而渲染又要吃 CPU 与内存,所以命中率下滑的影响是双重放大的。命中率掉十个百分点,回源量可能成倍上升,这句话值得当作容量规划的常识记住。
这笔账的取舍要点在于:缓存层的容量要按"能装下多少个变体"来算,而不是按"能装下多少页面"。变体数是缓存键各维度档位的乘积,如果键设计得不好,同样的存储装下的有效内容会大幅缩水。另外,回源带宽要按最坏情况估——也就是热点键集体失效且没有请求合并时的流量,而不是按平均命中率下的流量。
第三笔:构建与产物的账。预渲染与构建是 CPU 密集任务,构建期间会明显占用算力,如果构建机与在线渲染共用同一批资源,构建会在业务高峰之外再叠一层压力。因此构建时段要与业务高峰错开,尤其在有定时全量构建的场景下。产物方面,需要的是存储空间与版本保留策略:保留多少历史版本、多久清理一次、回滚时能切回多远,这些要有明确规则,否则存储会被历史产物慢慢吃满。
构建是否要单独一台机器,取决于构建频率与构建时长:改动频繁且构建耗时较长的,与在线渲染混布会互相干扰;改动很少且构建很快的,混布可以省一台机器。判断依据是构建期间的算力占用是否会让在线渲染的响应时间超出可接受范围,这一点需要按自身情况实测确认。
把前面的变量合起来,可以给出一套分区的判断逻辑。注意这是判断逻辑不是唯一答案,具体落点还要结合页面数量、更新频率与团队运维能力。
首页与列表页。对所有人一样(或只按地区、语言分档),更新频率中等,可缓存比例很高。这类页面是缓存收益最大的部分,适合放在离用户最近的层,配合主动失效;如果更新频率足够低,也可以走构建期生成。首页通常还是全站流量的大头,它的命中率直接影响整体回源量,值得优先做请求合并与提前刷新。
商品或内容详情页。主体对所有人一样,更新频率取决于业务;个性化部分通常只有价格档位、库存、推荐位几处。判断逻辑是先把主体推到可缓存的层,再把个性化字段单独异步取回。页面数量大时,如果更新频率低,预渲染的收益明显;如果更新频率高且页面数极大,预渲染的构建与存储成本会快速上升,此时更适合长 TTL 加主动失效,而不是全量预渲染。
搜索结果页。对单个查询串而言是"对所有人一样",但查询串的组合几乎是无限的,因此命中率天然不高,且长尾查询几乎没有复用价值。判断逻辑是:热门查询单独缓存,长尾查询直接回源或走客户端取数;同时要给结果页设较短的有效期,并对结果数量与分页深度设上限,避免深分页请求把源站拖住。搜索结果页通常不必纠结抓取问题,因为这类页面本来就不应该被大量索引。
用户中心与账户页。对每个用户都不一样,几乎没有可共享比例,也不需要被抓取。判断逻辑是这一类页面根本不该进入共享缓存,也不该为了抓取去做服务端渲染;放在源站按需渲染或直接在客户端取数都可以,重点是不要让它占用缓存层的容量,也不要让它进入主文档的缓存键。同时要给它单独设并发与超时限制,避免它拖垮共享的渲染进程池。
后台管理与报表。对每个用户或每个角色都不一样,更新频率取决于查询条件,完全不需要被抓取,也不需要首屏优化到极致。判断逻辑是彻底排除在共享缓存之外,并且最好与面向用户的渲染进程做资源隔离——后台的大范围查询与导出很容易吃掉内存与 CPU,让它与前台共用进程池是典型的相互拖累。报表类请求还应设独立的超时与排队上限。
活动落地页。这是最特殊的一类:平时几乎无流量,活动开始瞬间流量集中涌入,内容对所有人一样但更新极频繁(改价、加码、上下架)。判断逻辑是它必须预渲染或至少预缓存——在活动开始前就把产物准备好,而不是等第一个请求来了才渲染;同时必须有请求合并与静态兜底,因为它天然具备缓存击穿的全部条件。活动页的失效要走主动通道,因为价格与库存的陈旧窗口要求极短。
把这套判断抽象出来,就是一句话:按三个变量给页面分区,把可缓存的公开部分推到边缘或静态化,把不可缓存的个性化部分留在源站或客户端,而不是整体押注某一种渲染模式。
不要为了技术先进把所有页面都改成服务端渲染。全站统一 SSR 会把大量本可缓存的页面变成每次请求都要付出渲染算力,内存占用随并发渲染数增长,进程崩溃的影响面覆盖全站。首屏与抓取的收益只需要覆盖真正需要被抓取、且真正需要首屏的页面,其余页面用更便宜的方式处理即可。
不要把登录态放进缓存键,还期待高命中率。登录态一进键,页面就分裂成至少两个变体;用户标识一进键,缓存就直接退化为单人产物,命中率接近零而存储开销照旧。正确做法是把登录态相关的块单独异步取回,让主文档的键里只保留分组维度。
不要在没有失效机制的情况下设很长的有效期。长 TTL 的前提是存在主动失效通道,或者业务能接受那么长的陈旧窗口。两者都不具备却设长 TTL,结果是内容改了线上不生效,而排查时往往想不到是缓存的问题。反过来,只靠短 TTL 而不建失效机制,则要把回源压力长期背在身上。
不要让源站在没有请求合并时直接面对缓存击穿。热点键失效瞬间的并发回源足以压垮渲染进程,且会形成雪崩式的正反馈。请求合并、陈旧兜底、静态兜底页、提前异步刷新,这几项中至少要落实前两项,再谈渲染位置的优化。
本文只讨论渲染位置与缓存边界的取舍,以及它落在服务器上的资源账。以下三类问题不在本文范围内。
其一,本文不是前端框架选型对比。具体选用哪一个框架、框架的渲染性能如何、迁移成本有多大,这些问题有自己的评估维度。本文讨论的代价结构——常驻进程的内存、并发渲染数、缓存命中率——在任何框架上都成立,换框架不会改变"哪些页面该缓存"这个判断。
其二,本文不是内容分发网络或边缘计算产品的评测。文中提到边缘渲染只是为了说明一种形态的代价,不涉及任何具体平台的能力、覆盖范围或资费。是否需要引入这类服务、引入哪一家,取决于业务分布与预算,需要另行评估并向服务商确认。
其三,本文不是搜索引擎优化技巧清单。抓取友好的前提是初始响应里有内容,这一点本文会作为约束条件纳入判断,但标题写法、结构化数据标注、站点地图、抓取频次控制这些具体手法不在本文讨论范围内。
下表按"对谁变、多久变一次、可缓存比例"三个变量给出六类常见页面的落点判断。最后一列写的是判断与理由,不是一个技术名词,落点时仍需结合页面数量与团队运维能力调整。
| 内容类型 | 对谁变(所有人 / 分组 / 单人) | 更新频率 | 可被缓存的比例 | 建议的渲染位置与失效方式 |
|---|---|---|---|---|
| 首页与列表页 | 大多数情况下对所有人一样,个别版本按地区或语言分档 | 小时级到天级,栏目调整与置顶变更时不定期触发 | 很高,个性化通常只占顶部登录态条与少量推荐位 | 推到离用户最近的层做长有效期缓存,改版时用按标签主动失效立刻生效;因为它是全站流量大头,必须配请求合并与提前异步刷新,否则热点失效瞬间就会压垮源站。更新极慢的栏目页可改走构建期生成 |
| 商品或内容详情页 | 正文与参数对所有人一样;价格按会员等级分档;推荐位与足迹是单人维度 | 天级为主,改价、改库存、上下架时集中变化,频率不均匀 | 较高,主体可共享,仅价格档位、库存与推荐位不可共享 | 主体走可缓存层并设较长有效期,把库存与单人推荐位拆成小块单独异步取回;内容变更走按路径或按标签的主动失效,避免依赖纯过期。页面数极大且更新频繁时不要全量预渲染,否则构建与存储成本会超过收益 |
| 搜索结果页 | 同一查询串对所有人一样,但查询串组合近乎无限,实际复用集中在少数热词 | 分钟级到小时级,受索引更新与上下架影响 | 分化明显,热词部分可缓存,长尾部分几乎没有复用价值 | 只对热词做短有效期缓存,长尾直接回源或客户端取数;设分页深度与结果数量上限,防止深分页拖住源站。这类页面本就不应以被大量索引为目标,不必为抓取单独做服务端渲染 |
| 用户中心与账户页 | 对每个用户都不一样,单人维度占满整页 | 实时,任何操作都可能改变页面内容 | 接近零,没有可共享的部分 | 明确排除在共享缓存之外,放在源站按需渲染或客户端取数均可;关键是不让它进入主文档的缓存键、不占用缓存层容量,并为它单独设并发与超时上限,避免拖累共享的渲染进程池 |
| 后台管理与报表 | 按用户或角色不同,查询条件组合差异大 | 取决于查询条件,导出类请求往往是临时触发 | 接近零,且不需要首屏优化与抓取友好 | 完全不走共享缓存,并且应与面向用户的渲染进程做资源隔离;大范围查询与导出容易吃满内存和 CPU,与其共用进程池会相互拖累。需单独设排队上限、超时与熔断,慢查询不应长期占用槽位 |
| 活动落地页 | 对所有人一样,可能按地区或渠道分档 | 平时不变,活动期间分钟级变化,且集中在开始瞬间爆发 | 很高,但陈旧容忍度极低,价格与库存的窗口要求最短 | 活动开始前预渲染或预缓存好产物,绝不等第一个请求来了才渲染;失效必须走主动通道而非等过期;同时必须配请求合并、陈旧兜底与静态兜底页,因为它天然具备缓存击穿的全部条件 |
服务端渲染一定比客户端渲染快吗?不一定。服务端渲染改善的是首屏时间——浏览器第一次拿到响应时就有内容,不必再等脚本执行和数据请求。但它把渲染开销搬到了服务端,单次响应可能比返回静态文件更慢;如果页面数据依赖多个上游接口,服务端要等所有接口返回才能出 HTML,慢的那一环会拖住整页。客户端渲染则把这段开销分散到每台设备上。所以判断标准不是"哪种更快",而是"这个页面的首屏是否重要、是否需要被抓取、以及服务端是否扛得住这个并发渲染量"。
边缘渲染是不是一定更贵?本文不做价格判断,只说代价结构。边缘渲染缩短的是用户到渲染位置之间的往返,它并没有减少渲染本身的算力消耗,只是把算力从源站搬到了别处;同时它新增了边缘与源站之间的状态与缓存一致性这一项工作。是否划算取决于你的用户地理分布、回源链路长度、以及能否提高缓存命中率。如果命中率上不去,边缘渲染只是把开销换个地方发生。具体资费需向服务商确认。
个性化内容能不能走缓存?整页走不了,但拆开之后大部分能走。做法是判断页面里每一块属于"所有人、分组、单人"哪一档:属于前两档的进共享缓存,属于单人档的单独异步取回。只要主文档的缓存键里不含用户维度,命中率就不会被登录态拖垮。如果整页几乎都依赖用户身份,那这个页面本来就该放在不可缓存的那一层,不必强行拆分。
渲染进程的内存该怎么估?按结构估,不要按平均值估。内存占用大致等于进程的固定开销,加上"同时在渲染的页面数乘以单页渲染时的数据体积"。所以决定内存的是并发渲染数而不是请求总量——请求排队不吃内存,但同时开十个渲染上下文就吃。估算时要取高峰期的并发渲染数,再乘以单页数据体积的实际量级,并留出进程长期运行下的余量。具体取值需按自身页面与数据量实测确认。
缓存键里到底该放哪些维度?原则是"产物会因之而变才放"。通常必须有请求路径,可能还需要设备类型、地区、语言这些分组维度;查询参数要严格白名单化并做归一化,把来源标记、时间戳这类无关参数排除掉;登录态与用户标识原则上不应进键,应改用异步取回个性化块的方式处理。每多一个维度,变体数就乘一次,命中率降一档,存储占用则上升。
预渲染的构建压力有多大,要不要单独一台构建机?构建是 CPU 密集任务,耗时随页面数量增长,产物还要占存储并需要版本保留策略。是否单独一台机器,取决于构建频率、构建时长与在线渲染是否会被干扰:改动频繁且构建耗时长的,混布会在构建期间拖慢在线渲染,且构建时段必须与业务高峰错开;改动很少且构建很快的,混布可以省一台机器。判断依据是构建期间的在线响应时间是否超出可接受范围。
抓取方是不是必须拿到服务端渲染的 HTML?需要初始响应里就有正文的,就必须让内容出现在第一次响应中——服务端渲染、预渲染、或者缓存后的完整 HTML 都能满足这一点,三者选哪个取决于更新频率与页面数量。反过来,不需要被索引的页面(账户页、后台、搜索结果长尾)完全不必为此付出渲染代价。注意"预渲染的静态页"和"服务端渲染"在抓取效果上等价,不要因为要抓取就默认选服务端渲染。
缓存未命中并发回源把源站打爆了怎么办?先止血再治理。止血靠两件事:一是请求合并,同一个键在回源未返回前,后续请求挂起等待共用结果,而不是各自发起回源;二是降级兜底,渲染失败时返回上一版本产物(缓存项失效后保留为"陈旧但可用"而非直接删除),连上一版本都没有就返回不依赖后端数据的静态兜底页。治理则要靠提前异步刷新把热点键从不真正失效,以及给渲染进程设超时与熔断,避免慢请求占满槽位。
把全文的判断收成一句话:渲染位置的选择不是"服务端渲染好还是客户端渲染好"的二选一,而是按三个变量分区决定——内容对谁变、多久变一次、其中有多少比例可以被缓存;把可缓存的公开部分推到边缘或静态化,把不可缓存的个性化部分留在源站或客户端,比整体押注某一种渲染模式更能同时改善首屏、抓取与资源占用。
开篇那个"首屏快了但稳定性差了"的困境,根因不在渲染模式,而在"所有页面按同一种方式渲染"这个前提。内容对所有人相同的页面被放进每次请求都重新渲染的位置,是在用常驻的 CPU 与内存重复生产同一份产物;而真正需要每次重算的页面往往只占少数。按三个变量分区之后,大部分请求由缓存或静态产物承接,源站的渲染进程只需要处理少数不可缓存的请求,内存与 CPU 的压力结构随之改变,进程崩溃的影响面也被限制在少数页面上。
分区之后要补的三件事是固定的:缓存键要尽量少放维度,尤其不能放登录态;失效方式要按内容的变更能否被感知来选择,能挂标签就用主动失效,不能就用有效期兜底;任何存在缓存的位置都要有请求合并与降级兜底,否则一次热点失效就能抵消掉前面所有的优化。
一万网络深耕 IDC 行业 19 年(成立于 2007 年),在华南、华东、华北及中国香港、海外多地整合自营节点与 BGP 多线资源,提供服务器租用、机柜托管与带宽接入的一体化交付;具体机型、带宽与报价以官网实时信息或咨询确认为准。部署常驻渲染进程与构建任务时,建议按并发渲染数、单页数据体积与构建时长分别核算内存、CPU 与存储,具体机型与配置需结合页面量与发布频率确认。
本文的分析基于通用的 Web 系统设计与缓存设计原理,结合典型的渲染部署思路写成;文中出现的典型场景均为假设性描述,并非特指某一真实客户或某一次真实故障。文中未引用任何第三方统计数据、厂商实测数据或产品参数,也不对任何具体的框架、内容分发服务或边缘计算平台做推荐与比较。
关于具体阈值:本文刻意不给出内存容量、并发渲染数、有效期时长、缓存容量、构建时长等推荐数值。这些取值高度依赖页面复杂度、单页数据体积、页面总量与实际请求分布,正确做法是在接近真实的流量与数据上自行实测并按观测结果调整,本文只提供判断维度与取舍顺序。
关于命中率与回源量的关系:文中"命中率掉十个百分点,回源量可能成倍上升"描述的是结构关系而非精确比例,实际倍数取决于请求分布是否集中在少数热点键上。具体数值需按自身缓存键设计与访问分布实测得出。
关于承载与资源的成本:如果评估后决定为渲染进程配置独立承载、为构建任务单独准备算力、或需要更高的回源带宽与更大的产物存储,方案成本主要受 CPU、GPU、内存、存储、带宽、IP 与线路这些因素影响,且不同节点的资源单价与可选配置存在差异。这类支出应在选型阶段连同页面量、发布频率与增长预期一起核算,具体机型、带宽与报价以官网实时信息或咨询确认为准,文中不提供任何价格数字。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品