前提
- React/Reduxで動くSPAのWebサービス
- ページの構造は簡単なヘッダ、フッタ、サイドバーとメインのビュー構造
- node.jsで、SSRしてstreamで返すようにしてる
- styled-componentsでstyleをあててる
現象
SSRされたページのうち、サイドバーとフッタはstyleがあたらない。
正確には、SSRされたコンポーネントはヘッダとメインのリスト表示のみstyleがあたった状態で、サイドバー(右)とフッタはstyleがあたっていない状態で配信されてしまう。ただしその後クライアント側でhydrateされる段階で、styled-componentsのstyleがhtml上に展開されると全てのコンポーネントにstyleがあたる。このため、一瞬styleがあたっていない状態が見えるようなことが発生する。
原因
styled-componentsでstreamを使ったSSRを行う場合には、interleaveWithNodeStream()を利用する。これは、reactがSSRする際のreadableStreamをフックするような形で、streamの区切り(chunk)ごとにstyleをあてるtransform関数を適用している。
参考:Github: styled-components transform
しかし、streamのchunkはhighWatermarkの単位で分割されるわけだが、最初のchunkにstyled-componentのtransformが適用されると、それ以降のコンポーネントに対してstyleの適用がsealedされてしまい、そのままSSRされて配信されるためだと思われる。
styled-components v3.1.0の更新が関係してると思われるが、SSRされなくなるといった記載は見当たらない。
ちなみにこの更新は、insertRulesできるようになってjsでstyleを動的に追加するようにしたっていうのと、chunk的に分割して読み込めるようにすることで、SSRされる速度を高速化すると書いてあるように読めた。
参考:v3-1-0-such-perf-wow-many-streams
ちなみにstreamのchunkで分けられるサイズが大きければ問題が発生しなくなるか確認するために、試しに以下(streamのchunkサイズ)を2倍に変えたら、SSRで全てのコンポーネントにstyleがきくようになった。
参考:Github: ReactDOMNodeStreamRenderer.js#L23
this.push(this.partialRenderer.read(size * 2));
解決策
パッと思いつく方法は3つほどある。
-
あててるstyleを減らす
適用しようとしてるstyleの数が多いからchunkで分けられてしまうのであって、単純にcss部分を減らすことができれば問題は起きないと思われる。 -
何もしない
First-interactiveまでの時間が高速化されているのは事実なわけだし、SSRされたHTMLを開いた瞬間は確かにstyleがあたっていないけど、クライアント側でhydrateされるときにはきちんと表示されるわけだし、何もしないという手もある。 -
無理やり全てのstyleをあてる
今回は一応、これを選んで試してみてる。具体的には、interleaveWithNodeStream()で受け取ったstreamをstream.on('end' ,...)で読み終えてから、getStyleTags()で改めてcssを取得する。取得したstyleをinlineにHTMLに展開し、そのHTMLコンポーネントをrenderToStaticNodeStream()して配信する。こうすることで、全コンポーネントに対するstyleがきくようになった。
まとめ
- styled-componentsでSSR難しい&ややこしい
- 原因や解決策が本当にあっているのか自信があるわけではないので、何か間違っていることなどあればご指摘いただけると嬉しいです。
2018/09/12 追記
- 上記styled-componentsのバグは修正されました(v3.4.6)!
- テストが通らないことをIssue/PRとして出したら、修正してもらえました。
- どうやらServerStyleSheetのinstanceが持つtagMapに問題があったようです。
- SSRするタイミングでスタイルの読み込みをchunkごとにシールドしてtagMapに入れるはずが、
- コンポーネントを読みこんだ時点でtagMapを作ってしまっていたため、2つ目以降のchunkのstyleを取り込めなかった。
- 例えば、以下のようにすると動いた。
- 修正は、tagごとにchunkに取り込んだかどうかのフラグを持たせるようにしたみたいですね。
const sheet = new ServerStyleSheet()
+ sheet.instance.tagMap = {}
const jsx = sheet.collectStyles(