Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails + ReactなSPAサイトでのSEO / SEO at Rails + React SPA

adorechic
September 29, 2018

Rails + ReactなSPAサイトでのSEO / SEO at Rails + React SPA

概要
ReactやVue.jsを用いたSPAサイトが増え、SEOを目的としたSSRやパフォーマンスチューニングの事例も増えてきています。
一方で近年のフロントエンド技術の裾野は広く、同じことをやろうとしても技術スタックが異なる場合同じ手法が使えないということがままあります。
例えばReact-RailsはSSR機能を備えており、オプションで指定するだけでSSRが可能です。
しかし非同期フローとしてRedux-Sagaを採用していると、二回のrenderToStringが必要となり、かつ二回目はJSのコールバックで実行されるためそのままではReact-RailsでSSRすることができません。
もし仮にRailsでなくNode.jsを利用していればこのコールバックを自然に扱うすることができるため問題になりません。

最終的な構成の事例だけを見ていると、なぜそういった構成になっているかの経緯まで掴みきれず、いざ自分でやってみると躓くポイントは随所にあります。

そこで本セッションではRails + React + Redux + Redux-SagaなアプリケーションのSEO対策として、hypernovaによるSSRやパフォーマンス改善に取り組んだ事例について時系列でどういった背景によって構成を変えていったかを紹介します。

トピック
Universal JS
Redux-SagaをRailsでSSRする
React-Headをhypernova環境でSSRする
Code Splitting
SplitChunks(CommonsChunkPlugin) vs Dynamic Imports

adorechic

September 29, 2018
Tweet

More Decks by adorechic

Other Decks in Programming

Transcript

  1. ݱࡏͷߏ੒ w3FBDU w 3FEVY 4BHB 3FBDU3PVUFS 3FBDU)FBE w %ZOBNJD*NQPSUͰ$PEF4QMJUUJOH w

    XFCQBDLFS w $43༻ͱ443༻ͰXFCQBDLDPOpH͸݁ߏΧελϜ͍ͯ͠Δ w3BJMT IZQFSOPWB
  2. ΫϥΠΞϯταΠυϨϯμϦϯά αʔόʔ ϒϥ΢β EJWEJW EJW UBCMFDMBTTGPP  UBCMF EJW 'PP5BCMF

    IUNM +4 ΄΅ۭͷIUNMΛฦ͢ ΫϥΠΞϯτଆͰ3FBDU SFOEFSͯ͠ IUNMΛߏங͢Δ
  3. &YFD+4 w3VCZ͔Β+4Λ࣮ߦ͢Δ wϥϯλΠϜ͍Ζ͍Ζ w MJCWܥ UIFSVCZSBDFS NJOJ@SBDFS  w /PEFKT

    w FUD wศར͕͍ͩΖ͍Ζ੍ݶ͸͋Δ w +4&WFOUMPPQʹ׬શʹ͸ରԠ͓ͯ͠ΒͣɺTFU5JNFPVUͱ͔࢖͑ͳ͍
  4. ؀ڥґଘͷίʔυΛந৅Խ class DeviceDetector { constructor(userAgent) { // CSRではメディアクエリ、SSRではUAを使う if ((typeof

    window) === 'undefined') { this.detector = new UADetector(userAgent) } else { this.detector = new MediaDetector() } } } // メインコードでは環境を意識せずに使う deviceDetector = new DeviceDetector(this.props.user_agent) if (deviceDetector.isSmartPhone()) { ... }
  5. SFEVYTBHB w3FEVYͰඇಉظॲཧΛѻ͏NJEEMFXBSF class EstateComponent extends React.Component { handleClick() { //

    Reducerのときと同じようにdispatch dispatch({ type: 'REQUEST_FIND_ESTATE', params: { this.props.estateId } }) } } function* estateSaga() { yield takeEvery('REQUEST_FIND_ESTATE', fetchEstate) } function* fetchEstate(action) { try { const estate = yield call(API.fetchEstate, action.params.estateId) // 結果をreducerにdispatch yield put({ type: 'SUCCESS_FIND_ESTATE', estate }) } catch (e) { yield put({ type: 'FAILURE_FIND_ESTATE', message: e.message }) } }
  6. $43ͰͷTBHB ϒϥ΢β 3FBDU 4BHB 4BHBTUBSU SFOEFS EJTQBUDI "1*GFUDI 5BLFBDUJPO EJTQBUDI

    3FEVDFS 6QEBUFDPNQPOFOU ֓೦ͱͯ͠͸ ผʑͷεϨουͰ ಈ͘Πϝʔδ SFOEFS࣌ ൃߦ͞ΕͨBDUJPO܈Λ TBHB͕रͬͯ EJTQBUDIͯ͠໭͢
  7. ճSFOEFS͢Δ 3FBDU 4BHB SFOEFS EJTQBUDI "1*GFUDI 5BLFBDUJPO EJTQBUDI 3FEVDFS &/%BDUJPO͸

    ࣮ߦதͷTBHBλεΫ͕ ऴྃͨ͠ΒTBHB΋ऴྃ &/%EJTQBUDI 4BHBTUPQ 3FEVYTUPSF 3FBDU SFOEFS 3BJMT ॳճSFOEFSͨ͠Β &/%EJTQBUDI ̍ճ໨ͷ TUPSFΛ࢖ͬͯ ࠶౓SFOEFS
  8. 3FBDUIFBE +49Ͱهड़͓͚ͯ͠͹ ·ͱΊͯIFBEΛ࡞ͬͯ ͘ΕΔ const App = () => (

    <HeadProvider> <div className="Home"> <Title>Title of page</Title> <Link rel="canonical" content="..." /> <Meta name="example" content="..." /> // ... </div> </HeadProvider> )
  9. 3FBDUIFBEͷ443 BU/PEFKT const headTags = []; const app = renderToString(

    <HeadProvider headTags={headTags}> <App /> </HeadProvider> ); res.send(` <!doctype html> <head> ${renderToString(headTags)} </head> <body> <div id="root">${app}</div> </body> </html> `); SFOEFS͢Δͱ IFBE5BHTʹ IFBE༻ίϯϙʔω ϯτ͕ूΊΒΕΔ IFBE5BHT͚ͩΛ ࠶౓SFOEFSͯ͠ ϨΠΞ΢τʹຒΊ ࠐΉ
  10. )ZQFSOPWB͸EJWλάͷத਎͚ͩΛฦ͢ !!! %html %head %meta{:content => "text/html; charset=UTF-8"... ... %body

    ... = render_react_component('app', react_props) ϨΠΞ΢τ͸ 3BJMTଆʹ͋Δ IZQFSOPWBSVCZ͕ IZQFSOPWBTFSWFS ͔Βฦ͖ͬͯͨIUNM ยͰஔ׵ <div> <p>...</p> </div> SFBDUIFBEͰ ผ్SFOEFSͨ͠΋ͷΛ Ͳ͏΍ͬͯ3BJMTଆʹ౉͢ʁ
  11. Ұॹʹฦͯ͠ஔ׵ʢྗٕʣ !!! %html %head %meta{:content => "text/html; charset=UTF-8"... ... %hypernova-head-tags/

    %body ... = render_react_component('app', react_props) SFBDUIFBEʹSFOEFSͤ͞ ͨ΋ͷΛಠࣗλάͰҰॹ ʹฦ͢ IZQFSOPWBSVCZ͕ ஔ׵ͨ͠ޙʹ ϚʔΧʔλάΛஔ׵ <collected-hypernova-head-tags> ... </collected-hypernova-head-tags> <div> <p>...</p> </div>
  12. $PNNPOT$IVOL1MVHJO clientPlugins.push( new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: (m) => /node_modules/.test(m.context)

    }), ); clientPlugins.push( new webpack.optimize.CommonsChunkPlugin({ name: 'd3_chunk', minChunks: (m) => /node_modules\/(?:d3|redux)/.test(m.context) }), ); clientPlugins.push( new webpack.optimize.CommonsChunkPlugin({ name: 'redux_chunk', minChunks: (m) => /node_modules\/(?:redux)/.test(m.context) }), ); ໊લϕʔεͰϚον ޷͖ͳཻ౓Ͱ DIVOLʹͰ͖Δɺ͕ Ϛονͷ฼ूஂ͕ લճͷϚονର৅ͳͷͰ ্ྲྀ͸ԼྲྀΛશؚͯΉ
  13. %ZOBNJD*NQPSUT import(/* webpackChunkName: "raven" */ 'raven-js').then((Raven) => { Raven.config( 'endpoint',

    { environment: this.props.railsEnv, release: this.props.revision } ).install() })
  14. SFBDUMPBEBCMF w3FBDUίϯϙʔωϯτΛEZOBNJDJNQPSUT ʹͯ͘͠ΕΔ)0$ϥΠϒϥϦ import Loadable from 'react-loadable' const Lightbox =

    Loadable({ loader: () => import(/* webpackChunkName: "react-images" */ 'react-images'), loading() { return <div>Loading...</div> } }) ˞MPBEBCMFDPNQPOFOUTͱ͔΋Αͦ͞͏