likes
comments
collection
share

解决服务端渲染时 antd 导致样式闪烁的问题

作者站长头像
站长
· 阅读数 9

问题

通过 SSG 生成的页面在首屏渲染时,样式会发生抖动。

解决过程

  • 首先怀疑是 docusaurus 的问题(遇到的问题确实大多数都是 docusaurus 的问题)。但是翻阅了一下 docusaurus 的 issue 和文档,并没有看到相关的描述。

  • 于是观察页面抖动的行为,发现产生抖动的都是 antd 相关的组件。聚焦到 antd 上,改版后的 antd5 采用的是 css-in-js 方案,查看网页源码发现里面并没有 antd 相关的样式,查阅资料后发现页面需要使用的 css 是通过 js 来生成的!也就是说渲染页面时,直接将页面渲染了出来,之后再通过 js 生成样式,在页面渲染出来到样式生成的这段时间是没有 antd 样式的,所以在样式加载完成后会出现样式的闪烁。

  • 查看 antd 的 issus 列表,发现确实存在服务端渲染的问题。在官方文档上找到了解决方案,我们可以通过官方提供的 extractStyle 方法导出 css。但由于我们是 SSG 而非 SSR,所以无法在服务端动态处理并将按需将 css 加入到返回的 html 中。我们只能在 start 或 build 前,执行一个 ts 脚本,生成 antd.css,并在项目中引入该 css。

  • 按照上文的方法改动后,本地运行样式确实不闪烁了,非常好,本帖终结!

  • 直到我将项目 build 起来,才发现访问 build 后的 html 依旧样式闪烁...fine,继续 debug。查看 build 后的代码,我的 antd 样式确实已经注入进去了,但是为什么没有生效呢?

  • 仔细比对注入的 css 和 antd 通过 js 生成的 css,发现主动注入的css有如下css select :where(.css-1cxilis),而动态生成的 css 是:where(.css-3reisqm)!页面上 antd 相关的 dom 确实有css-3reisqm这么一个类,所以说我们提前注入的样式确实是失效的!

  • 到这里结果很明显了,我本地跑脚本执行 extractStyle 的时候,自动生成了一个 hash,猜测 build 执行过程中又生成了一个 hash,导致两边的 hash 不一致。正常的 SSR 由于在服务端执行extractStyle,并通过 cache 达到生成的 hash 一致,而我们使用的 SSG 没办法去自定义 build 过程,所以两次 hash 不一致了...

  • 去翻阅了 antd 的 issue,里面服务端渲染几乎全部都是 SSR,没有 SSG 相关的,fine...

  • 那换一种思路,虽然无法控制 build 过程,但可以控制注入 css 的过程啊。于是我们可以在 build 完成后读取 build 后的 html,通过正则匹配出 build 使用的 hash。接着在我们执行 extractStyle 后,将结果中的 hash 替换为 build 的 hash 即可,然后可以将生成的 css 注入到 build 后每个页面都会引用的 style.xxx.css 中

  • 按照上面的思路做完后,终于,访问 build 产物不再出现样式抖动了。此贴完结撒花!

  • 没高兴多久,又发现了新的问题:我自定义的 antd 主题怎么失效了?查看页面上的 button 样式,发现动态生成的 css 是带正确主题的,但是我们提前注入的 css 并没有带主题,而是使用默认主题!而且,由于动态生成的 style 标签在上,而引入的 style.xxx.css 在下,默认主题把正确的主题覆盖掉了。

  • 首先,为什么我们生成的css主题丢失了呢?我们查看extractStyle的产物,发现主题确实没有注入进去,这看起来像是官方提供的这个方法有问题。果然,我在 issue 列表中找到了相同的issue,官方似乎至今还没有解决这个问题。

  • 那么,有什么办法可以在官方修复问题之前跑起来呢,毕竟快上线了...

  • 既然是样式覆盖问题,那有没有办法在让自动生成的 css 优先级比注入的高呢?查阅资料,发现有 StyleProvider 组件,在里面可以设置 hashPriority,也就是说我可以在外层套一个 StyleProvider 并设置 hashPriority=high提高自动生成的 css 优先级。按照这个方法做完后,页面上的主题终于正确了,但是又引入了新的问题:antd的全局样式均为 a.css-xxxx这种,所以优先级会很高,覆盖掉了我们很多自定的全局样式!

  • 既然主动提高优先级的路行不通,那可以从 css 顺序上下功夫,我们知道相同的优先级,总是后面的样式覆盖前面的样式。那么我们可以将注入的 antd 样式放在最前面,让自动生成的css在后面,这样优先级更高。为此,我们需要将 antd 样式从 style.xxx.css 中独立出来,否则我们定义的很多全局样式又被 antd 覆盖掉了。

  • 为此,又写了一个脚本,先生成一个 antd.css,然后遍历整个 build 产物,在里面每个 html 文件的<head>首行注入<link rel="stylesheet" href="/assets/css/antd.css">,这样样式就被单独注入到<head>头部了。正当我庆幸此贴终于能终结时,现实狠狠给了我一个大嘴巴子。antd自动生成样式时,总是会生成在最顶部!也就是说不管我 antd.css 放在哪里,总是在自动生成的样式后面!啊这...

  • 当我快要放弃时,一个声音告诉自己:都到这一步了,沉没成本太高,不甘心啊!郁闷的我刚准备体验一下带薪拉屎,脑中突然灵光一显:既然注入的 css 优先级更高,那我在页面加载完成后把它删了不就得了,你高任你告,我直接连根拔。于是在 useEffect 中将主动注入的 style remove 掉,终于,主题生效了!

  • 当前的表现为首屏布局以及样式都时正常的,只会主题色等在页面加载完成后稍微变动一下。比起完全没有样式->有样式,默认主题->自定义主题还能接受了。不过还是等官方修复extractStyle这个方法的问题吧,修好后我这个临时代码就可以寿终正寝了。

解决方案

  1. 通过脚本对 build 产物进行处理:先通过extractStyle生成一个 antd.css,然后遍历整个 build 产物,在里面每个 html 文件的<head>首行注入<link rel="stylesheet" href="/assets/css/antd.css">
  2. 在页面加载完成后将注入的css样式移除

终极解决方案

移除 antd。 是的你没看错,其实作为一个 SSG 项目来说,css-in-js 方案天然就不合适,更不用提作为一个 ToC 项目来说 antd 对全局样式的入侵。antd的适用场景感觉还是后台或者对样式要求不高的项目。所以,下一步的计划就是逐渐通过自定义组件或小而精的特定组件库(如react-modal)替换掉 antd 组件。