renderToString()問題を解決した後、もう一度負荷試験を実施しました。しかし、負荷をしばらくかけ続けたところ、レスポンスタイムがだんだん長くなってきてしまいました。
この際ブラウザでページにアクセスしてみると、返されたページの中に重複しているデータが多く、メモリリークだということが分かりました。
調査してみると、問題はReactの書き方にあることが判明しました。ネット上で検索すると、ES6(ECMAScript 2015)の「defaultProps」の書き方は、ほとんど下記2つのいずれかのようになります。
export class SomeComponent1 extends React.Component { static defaultProps = { // ... } // ... }
class SomeComponent2 extends React.Component { // ... } SomeComponent2.defaultProps = { // ... } export SomeComponent2;
しかし、この場合、defaultPropsはシングルトンになります。
そのため、defaultPropsに配列が入っている際にサーバ上で配列にどんどん新しい値をプッシュしたら、メモリリークが発生しました。以下の書き方で解決しました。
export class SomeComponent1 extends React.Component { static get defaultProps() { return { // defaultProps }; } // ... }
こうすることで、毎回defaultPropsを使うたびに新しいオブジェクトを生成するようになるので、配列を使うことによるメモリリークを避けられます。
メモリリークは、ほとんどの場合はデータ量が少ないため、フロントエンドでは観察しにくいものです。そのため、サーバで負荷をしばらくかけ続けてメモリリークが発生するかどうかを確認するといいでしょう。
【チューニングポイント3】キャッシュの設計で説明したキャッシュサーバは、最初に「Redis」を採用しました。Redisは機能が多く、操作しやすいのが特徴です。一方で負荷をかけ続けると、メモリ使用量も増え続けるという特徴もあります。
Redisを使い、負荷をかけ続ける途中、あるタイミングでレスポンスタイムが一気に高騰することがありました。DatadogでRedisのEviction(データの追い出し)状況を見ると、レスポンス高騰とEvictionの発動タイミングが完全に一致していたことが分かりました。特にデータサイズが10KBを超える際は、Redisのパフォーマンスが急激に下がります。
調べてみると、単なるキャッシュサーバなら、「memcached」の方が良さそうだということに気付きました。memcachedに切り替えて、この問題を解決しました。
適切な負荷試験を行うことで、サーバサイドのパフォーマンスの問題がほとんど分かります。フロントエンドのパフォーマンスの問題はサーバサイドのようにすぐに分かるものではありません。ただアメブロは開発途中、毎回Chromeでページを開くたびに少しSPAで遷移すると、すぐにページが重くなってしまう問題がありました。
Chrome Dev Toolを使ってCPU状況を記録し、SPAで何ページか遷移して、最後にストップをクリックすると、JavaScriptの「Call Tree」が表示されます。そこでメソッドのコール状況が分かります。重いメソッドを最後まで展開して、呼び出し層がかなり深い場合は、メモリリークになる可能性が高くなります。
アメブロの場合はReactのライフサイクルにより、メモリリークが発生しました。
Reactはコンポーネント内部のstateが更新されると、「shouldComponentUpdate()」→「componentWillUpdate()」→「render()」という順でメソッドが実行されます。「componentWillUpdate()」「render()」の中でstateをさらに更新したら、もう1回処理が走り、無限ループになる原因となります。無限ループになった場合、コールスタックがいっぱいになり、メモリ使用量が急増してCPUも圧迫されます。
ただChrome Dev Toolで最後までメソッドを展開することで、メモリリークの場所は必ず見つかると思います。
このように、今回のリニューアルではさまざまなチューニングを行いましたが、そのタイミングは「開発の後半」にして、ある程度システムができてから負荷試験やチューニングを行うことにしました。かつてDonald Kruth氏が「早過ぎる最適化は諸悪の根源である」と言っていました。おかげで今回の開発がスムーズに進められました。
また、Isomorphic Web Applicationのパフォーマンスチューニングにおける大きな注意点の1つは、サーバサイドとフロントエンドの両方をチューニングしなければいけない点です。特にサーバサイドは影響範囲が広いので、さらに大事だといえるでしょう。
加えて、Isomorphic Web Applicationのアーキテクチャの構築とチューニングは従来の単なるSSRとSPAの開発より、難易度が少し上がるように思いますが、SSRとSPA両方のメリットを持っているため、SSRとSPAの限界をなくしSEOもUXも大幅に向上できます。
今回のリニューアルで行ったチューニングに関しては以上ですが、われわれは現状のパフォーマンスに関して満足しているわけではありません。特にReactの同期的なレンダリングメソッドでは、サーバサイドのレンダリングが遅くなるし、ブラウザ上のFPSも低くなります。
そのためReactのコミュニティーでは既に「React Fiber」というアーキテクチャを提案し、開発中のようです。この点については、非常に大きな期待を抱いています。
また今回、アメブロはJavaScriptの「async/await」を採用しましたが、Chrome Dev Toolで調べると、「async/await」はGeneratorを使っています。またGeneratorは、まだChromeに最適化されていません。ただ、V8 5.7は「async/await」のパフォーマンスが大幅改善されそうです。これからNode.js 8.0は新しいV8を採用する予定ですので、この点も期待しています。
今後、「アメブロ」では新しい技術の採用にチャレンジしつつ、世界中の開発者と協力しながら、より良いサービスを提供することを目指しています。
Copyright © ITmedia, Inc. All Rights Reserved.