Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Server Side Rendering of JavaScript in PHP

666 views

Published on

What is SSR, which problems does it solve, why do it in PHP, what options do we have for it, libraries that are available and tips and tricks. Practical code examples for Symfony and React.js, but the fundamental points can be taken away to use in other stacks like Vue and Laravel.

Published in: Software
  • Be the first to comment

Server Side Rendering of JavaScript in PHP

  1. 1. Server-side Rendering of JavaScript in PHP Nacho Martín
  2. 2. Nacho Martín I write code at Limenius. We build tailor-made projects, and provide consultancy and formation. We are very happy with React, and have been dealing with how to integrate with PHP for some time now & publishing libraries about it.
  3. 3. What is the problem that Server Side Rendering adresses?
  4. 4. Nacho Martín nacho@limenius.com @nacmartin A long time ago in a galaxy far, far away Server
  5. 5. Nacho Martín nacho@limenius.com @nacmartin A long time ago in a galaxy far, far away Server HTML </> HTML </> Client
  6. 6. Nacho Martín nacho@limenius.com @nacmartin Very simple model that works But to show a change we have to do a full page reload. Even for things like “your password must have at least 6 characters”.
  7. 7. Nacho Martín nacho@limenius.com @nacmartin Even for things like this New password New Submit
  8. 8. Nacho Martín nacho@limenius.com @nacmartin Even for things like this New password New Submit ****
  9. 9. Nacho Martín nacho@limenius.com @nacmartin Even for things like this New password New Submit **** Your password must contain 6 characters!
  10. 10. Nacho Martín nacho@limenius.com @nacmartin Adding dynamic elements HTML </> Client HTML </> Server
  11. 11. Nacho Martín nacho@limenius.com @nacmartin Adding dynamic elements HTML </> Client HTML </> JS JS Server
  12. 12. Nacho Martín nacho@limenius.com @nacmartin Step 1: Client uses JS to modify the DOM Client HTML </> JS $( "p" ).addClass( “myClass" );
  13. 13. Nacho Martín nacho@limenius.com @nacmartin With DOM modification We can now modify the document reacting to user interaction. What about loading new content based on content interaction?
  14. 14. Nacho Martín nacho@limenius.com @nacmartin Example 1 2 3 4 5
  15. 15. Nacho Martín nacho@limenius.com @nacmartin Adding dynamic content HTML </> Client HTML </> JS JS Server
  16. 16. Nacho Martín nacho@limenius.com @nacmartin Adding dynamic content HTML </> Client HTML </> JS JS Server API
  17. 17. Nacho Martín nacho@limenius.com @nacmartin Step 2: Dynamic content Client HTML </> JS $(“#grid").load( “api/page2.html“ ); API
  18. 18. Nacho Martín nacho@limenius.com @nacmartin Step 2: Dynamic content Client HTML </> JS $(“#grid").load( “api/page2.html“ ); API $.get( “api/page2.json“, function(data) { $(“#grid”).html(renderPage(data)); } );
  19. 19. Nacho Martín nacho@limenius.com @nacmartin DOM Manipulation HTML </> This happens in the Browser Element <body> Element <div id=“grid”> Element <h1> Text “Hi there” … Element <div> Element <div>
  20. 20. Nacho Martín nacho@limenius.com @nacmartin DOM Manipulation This happens in the Browser Element <body> Element <div id=“grid”> Element <h1> Text “Hi there” … Element <div> Element <div> API $.get( “api/page2.json“, function(data) { $(“#grid”).html(renderPage(data)); } );
  21. 21. Nacho Martín nacho@limenius.com @nacmartin DOM Manipulation This happens in the Browser Element <body> Element <div id=“grid”> Element <h1> Text “Hi there” API $.get( “api/page2.json“, function(data) { $(“#grid”).html(renderPage(data)); } );
  22. 22. Nacho Martín nacho@limenius.com @nacmartin DOM Manipulation This happens in the Browser Element <body> Element <div id=“grid”> Element <h1> Text “Hi there” … Element <div> Element <div> API $.get( “api/page2.json“, function(data) { $(“#grid”).html(renderPage(data)); } );
  23. 23. Nacho Martín nacho@limenius.com @nacmartin Problem: duplication of work HTML </> We need a mechanism in the server to build the initial HTML…
  24. 24. Nacho Martín nacho@limenius.com @nacmartin Problem: duplication of work …and another to update the DOM in the client API $(“#grid”).html(renderPage(data));
  25. 25. Nacho Martín nacho@limenius.com @nacmartin Possible solution: don’t render the content in HTML HTML </> <div id=“grid”>
  26. 26. Nacho Martín nacho@limenius.com @nacmartin And on document load do an API call <div id=“grid”> API $(“#grid”).html(renderPage(data));
  27. 27. Nacho Martín nacho@limenius.com @nacmartin This means that the first thing the user sees is this …and also crawlers :(
  28. 28. Nacho Martín nacho@limenius.com @nacmartin Slow page loads in mobile users https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/ • Average load time over 3G: 19 seconds. • 53% of sites that take longer than 3s are abandoned. • Going from 19s to 5s means: • 25% more impressions of ads. • 70% longer sessions. • 35% lower bounce race. • 2x ad revenue.
  29. 29. Nacho Martín nacho@limenius.com @nacmartin When are these problems worse Apps. Bearable. Content pages. Probably unbearable.
  30. 30. Concurrent problem: DOM manipulation vs State based JS libraries
  31. 31. Nacho Martín nacho@limenius.com @nacmartin We want to build a TODO list Pour eggs in the pan How to cook an omelette Buy eggs Break eggs
  32. 32. Nacho Martín nacho@limenius.com @nacmartin We want to build a TODO list Pour eggs in the pan Beat eggs How to cook an omelette Buy eggs Break eggs
  33. 33. Nacho Martín nacho@limenius.com @nacmartin Options
  34. 34. Nacho Martín nacho@limenius.com @nacmartin Options 1: Re-render everything.
  35. 35. Nacho Martín nacho@limenius.com @nacmartin Options 1: Re-render everything. Simple
  36. 36. Nacho Martín nacho@limenius.com @nacmartin Options 1: Re-render everything. Simple Not efficient
  37. 37. Nacho Martín nacho@limenius.com @nacmartin Options 2: Find in the DOM where to insert elements, what to move, what to remove… 1: Re-render everything. Simple Not efficient
  38. 38. Nacho Martín nacho@limenius.com @nacmartin Options 2: Find in the DOM where to insert elements, what to move, what to remove… 1: Re-render everything. Simple Complex Not efficient
  39. 39. Nacho Martín nacho@limenius.com @nacmartin Options 2: Find in the DOM where to insert elements, what to move, what to remove… 1: Re-render everything. Simple EfficientComplex Not efficient
  40. 40. Nacho Martín nacho@limenius.com @nacmartin Options 2: Find in the DOM where to insert elements, what to move, what to remove… 1: Re-render everything. Simple EfficientComplex Not efficient React allows us to do 1, although it does 2 behind the scenes
  41. 41. Nacho Martín nacho@limenius.com @nacmartin Fundamental premise Give me a state and a render() method that depends on it and forget about how and when to render.
  42. 42. Nacho Martín nacho@limenius.com @nacmartin Let’s write a React component Click me! Clicks: 0
  43. 43. Nacho Martín nacho@limenius.com @nacmartin Let’s write a React component Click me! Clicks: 1Click me!
  44. 44. Nacho Martín nacho@limenius.com @nacmartin Let’s write a React component import React, { Component } from 'react'; class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; } tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); } } export default Counter;
  45. 45. Nacho Martín nacho@limenius.com @nacmartin Let’s write a React component import React, { Component } from 'react'; class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; } tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); } } export default Counter; Initial state
  46. 46. Nacho Martín nacho@limenius.com @nacmartin Let’s write a React component import React, { Component } from 'react'; class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; } tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); } } export default Counter; Set new state Initial state
  47. 47. Nacho Martín nacho@limenius.com @nacmartin Let’s write a React component import React, { Component } from 'react'; class Counter extends Component { constructor(props) { super(props); this.state = {count: 1}; } tick() { this.setState({count: this.state.count + 1}); } render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); } } export default Counter; Set new state render(), called by React Initial state
  48. 48. Nacho Martín nacho@limenius.com @nacmartin Working with state constructor(props) { super(props); this.state = {count: 1}; } Initial state
  49. 49. Nacho Martín nacho@limenius.com @nacmartin Working with state constructor(props) { super(props); this.state = {count: 1}; } Initial state this.setState({count: this.state.count + 1}); Assign state
  50. 50. Nacho Martín nacho@limenius.com @nacmartin render() and JSX render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Clícame!</button> <span>Clicks: {this.state.count}</span> </div> ); It is not HTML, it is JSX. React transforms it internally to HTML elements. Good practice: make render() as clean as possible, only a return.
  51. 51. Nacho Martín nacho@limenius.com @nacmartin render() and JSX render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); }
  52. 52. Nacho Martín nacho@limenius.com @nacmartin render() and JSX render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); } Here we don’t modify the state
  53. 53. Nacho Martín nacho@limenius.com @nacmartin render() and JSX render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); } Here we don’t make Ajax calls
  54. 54. Nacho Martín nacho@limenius.com @nacmartin render() and JSX render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}>Click me!</button> <span>Clicks: {this.state.count}</span> </div> ); } Here we don’t calculate decimals of PI and send an e-mail with the result
  55. 55. Nacho Martín nacho@limenius.com @nacmartin Components hierarchy
  56. 56. Nacho Martín nacho@limenius.com @nacmartin Components hierarchy
  57. 57. Nacho Martín nacho@limenius.com @nacmartin Components hierarchy: props class CounterGroup extends Component { render() { return ( <div> <Counter name="amigo"/> <Counter name="señor"/> </div> ); } }
  58. 58. Nacho Martín nacho@limenius.com @nacmartin Components hierarchy: props render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}> Click me! {this.props.name} </button> <span>Clicks: {this.state.count}</span> </div> ); } and in Counter… class CounterGroup extends Component { render() { return ( <div> <Counter name="amigo"/> <Counter name="señor"/> </div> ); } }
  59. 59. Nacho Martín nacho@limenius.com @nacmartin Components hierarchy: props render() { return ( <div className="App"> <button onClick={this.tick.bind(this)}> Click me! {this.props.name} </button> <span>Clicks: {this.state.count}</span> </div> ); } and in Counter… class CounterGroup extends Component { render() { return ( <div> <Counter name="amigo"/> <Counter name="señor"/> </div> ); } }
  60. 60. Nacho Martín nacho@limenius.com @nacmartin Everything depends on the state, therefore we can:
  61. 61. Nacho Martín nacho@limenius.com @nacmartin Everything depends on the state, therefore we can: •Reproduce states,
  62. 62. Nacho Martín nacho@limenius.com @nacmartin Everything depends on the state, therefore we can: •Reproduce states, •Rewind,
  63. 63. Nacho Martín nacho@limenius.com @nacmartin Everything depends on the state, therefore we can: •Reproduce states, •Rewind, •Log state changes,
  64. 64. Nacho Martín nacho@limenius.com @nacmartin Everything depends on the state, therefore we can: •Reproduce states, •Rewind, •Log state changes, •…
  65. 65. Nacho Martín nacho@limenius.com @nacmartin Everything depends on the state, therefore we can
  66. 66. Nacho Martín nacho@limenius.com @nacmartin Everything depends on the state, therefore we can render the initial state to a HTML string
  67. 67. Server side rendering
  68. 68. Nacho Martín nacho@limenius.com @nacmartin First page load HTML </> Client HTML </> JS JS Server API Includes initial state
  69. 69. Nacho Martín nacho@limenius.com @nacmartin When we need more data Client Server APIAPI API calls to update the state and therefore update its representation (UI)
  70. 70. Nacho Martín nacho@limenius.com @nacmartin ReactDOMServer.renderToString(element) ReactDOM.hydrate(element, container[, callback]) SSR in React
  71. 71. Nacho Martín nacho@limenius.com @nacmartin ReactDOMServer.renderToString(<MyApp/>) SSR in React. 1) In the server: <div data-reactroot=""> This is some <span>server-generated</span> <span>HTML.</span> </div>
  72. 72. Nacho Martín nacho@limenius.com @nacmartin SSR in React. 2) insert in our template <html> … <body> <div id=“root”> <div data-reactroot=""> This is some <span>server-generated</span> <span>HTML.</span> </div> </div> …
  73. 73. Nacho Martín nacho@limenius.com @nacmartin ReactDOM.hydrate( <MyApp/>, document.getElementById('root') ) SSR in React. 1) In the client: <div id=“root”> <div data-reactroot=""> This is some <span>server-generated</span> <span>HTML.</span> </div> </div> … The client takes control over it
  74. 74. Nacho Martín nacho@limenius.com @nacmartin renderer.renderToString(app, (err, html) => { if (err) throw err console.log(html) // => <div data-server-rendered="true">Hello World</div> }) var app = new Vue({ el: ‘#root', data: { message: 'Hello Vue!' } }) Similar, in Vue
  75. 75. Nacho Martín nacho@limenius.com @nacmartin React, Vue, but does my library support this? If what is rendered depends on the state then we are probably good. If your JS depends on having the DOM loaded and manipulate it, then we are not good.
  76. 76. Nacho Martín nacho@limenius.com @nacmartin Problematic Example Page
  77. 77. Nacho Martín nacho@limenius.com @nacmartin Problematic Example Page API My React Component
  78. 78. Nacho Martín nacho@limenius.com @nacmartin Problematic Example Page API My React Component API “React Component” that renders CkEditor, d3, or other lib that relies on DOM manipulation
  79. 79. Nacho Martín nacho@limenius.com @nacmartin Problematic Example Page API My React Component API “React Component” that renders CkEditor, d3, or other lib that relies on DOM manipulation
  80. 80. SSR in PHP
  81. 81. Nacho Martín nacho@limenius.com @nacmartin Why? SSR in JavaScript (node.js) is more natural. But it may not be the right choice for the project. Maybe the team has expertise in PHP. Maybe the project already exists. Maybe we want to combine it with sections rendered from PHP. The Real World is not as simple as tutorials.
  82. 82. Nacho Martín nacho@limenius.com @nacmartin We need
  83. 83. Nacho Martín nacho@limenius.com @nacmartin WebpackAssets JS JS TS SASS PNG JPEG JS Client App
  84. 84. Nacho Martín nacho@limenius.com @nacmartin WebpackAssets JS JS TS SASS PNG JPEG JS Client App JS Server side App
  85. 85. Nacho Martín nacho@limenius.com @nacmartin JS Code to execute Server side JS App + Component and state that we want to render A few bytes, Changes between requests As big as your app. Doesn’t change between requests
  86. 86. Options
  87. 87. Nacho Martín nacho@limenius.com @nacmartin This is what we would do for SSR in JS Client JS Front PHP API JS Client side App Server side JS App + Component and state
  88. 88. Nacho Martín nacho@limenius.com @nacmartin This is what we would do for SSR in JS Client JS Front PHP API JS Client side App Server side JS App + Component and state
  89. 89. Nacho Martín nacho@limenius.com @nacmartin This is what we would do for SSR in JS Client JS Front PHP API JS Client side App Server side JS App + Component and state
  90. 90. Nacho Martín nacho@limenius.com @nacmartin Option 1: node.js as subprocess Client PHP App Node.js JS Client side App Server side JS App + Component and state
  91. 91. Nacho Martín nacho@limenius.com @nacmartin Option 1: node.js as subprocess Client PHP App Node.js JS Client side App Server side JS App + Component and state
  92. 92. Nacho Martín nacho@limenius.com @nacmartin Make a call to node.js using Symfony Process component * Easy to setup. * Slow. Library: https://github.com/nacmartin/phpexecjs Option 1: Call a node.js subprocess
  93. 93. Nacho Martín nacho@limenius.com @nacmartin Option 2: V8JS Client PHP App V8js JS Client side App Server side JS App + Component and state
  94. 94. Nacho Martín nacho@limenius.com @nacmartin Option 2: V8JS Client PHP App V8js JS Client side App Server side JS App + Component and state
  95. 95. Nacho Martín nacho@limenius.com @nacmartin But we can cache public function createContext($code, $cachename = null) { if ($cachename) { $cacheItem = $this->cache->getItem($cachename); if ($cacheItem->isHit()) { $snapshot = $cacheItem->get(); } else { $snapshot = V8Js::createSnapshot($code); $cacheItem->set($snapshot); $this->cache->save($cacheItem); } } else { $snapshot = V8Js::createSnapshot($code); } $this->v8 = new V8Js('PHP', [], [], true, $snapshot); }
  96. 96. Nacho Martín nacho@limenius.com @nacmartin Option 2: V8JS Client PHP App V8js JS Client side App Server side JS App Component and state
  97. 97. Nacho Martín nacho@limenius.com @nacmartin Option 2: V8JS Client PHP App V8js JS Client side App Server side JS App Component and state 📷
  98. 98. Nacho Martín nacho@limenius.com @nacmartin Option 2: V8JS Client PHP App V8js JS Client side App Server side JS App Component and state 📷
  99. 99. Nacho Martín nacho@limenius.com @nacmartin Use PHP extension v8js * Fast. * Need to compile v8, v8js, find Docker images… (this problem is not small). Library: https://github.com/nacmartin/phpexecjs Option 2: v8js PHP extension
  100. 100. Nacho Martín nacho@limenius.com @nacmartin Option 3: External JS server Client PHP App JS renderer JS Client side App Server side JS App Component and state
  101. 101. Nacho Martín nacho@limenius.com @nacmartin Option 3: External JS server Client PHP App JS renderer JS Client side App Server side JS App Component and state
  102. 102. Nacho Martín nacho@limenius.com @nacmartin We have “stupid” node.js server used only to render components. It has <100 LoC, and it doesn’t know anything about our logic. * Fast. There is an example a dummy JS server for this purpose at https://github.com/Limenius/symfony-react-sandbox Option 3: External node.js server
  103. 103. ReactRenderer & ReactBundle
  104. 104. Nacho Martín nacho@limenius.com @nacmartin Some libraries phpexecjsReactRendererReactBundle node.js v8js … Twig extension External renderer Selects JS runner Runs it Uses snapshots if available Integration with Symfony
  105. 105. Nacho Martín nacho@limenius.com @nacmartin Options 1 & 2 $renderer = new PhpExecJsReactRenderer(‘path_to/server-bundle.js’); $ext = new ReactRenderExtension($renderer, 'both'); $twig->addExtension($ext); phpexecjs detects the presence of the extension v8js, if not, calls node.js
  106. 106. Nacho Martín nacho@limenius.com @nacmartin Option 3: external renderer $renderer = new ExternalServerReactRenderer(‘../some_path/node.sock’); $ext = new ReactRenderExtension($renderer, 'both'); $twig->addExtension($ext);
  107. 107. Nacho Martín nacho@limenius.com @nacmartin JS side part: React on Rails https://github.com/shakacode/react_on_rails Used among others by
  108. 108. Nacho Martín nacho@limenius.com @nacmartin JS side part: React on Rails {{ react_component('RecipesApp', {'props': props}) }} import ReactOnRails from 'react-on-rails'; import RecipesApp from './RecipesAppServer'; ReactOnRails.register({ RecipesApp }); Twig: JavaScript:
  109. 109. Nacho Martín nacho@limenius.com @nacmartin Redux integration
  110. 110. Nacho Martín nacho@limenius.com @nacmartin Redux integration Redux Store
  111. 111. Nacho Martín nacho@limenius.com @nacmartin Redux integration import ReactOnRails from 'react-on-rails' import RecipesAppRedux from './RecipesApp' import configureStore from ' ../store/RecipesStore' const recipesStore = configureStore ReactOnRails.registerStore({ recipesStore }) ReactOnRails.register({ RecipesAppRedux }) Twig: JavaScript: {{ redux_store(‘recipesStore’, initialState) }} {{ react_component('RecipesAppRedux') }}
  112. 112. Nacho Martín nacho@limenius.com @nacmartin Share store between components
  113. 113. Nacho Martín nacho@limenius.com @nacmartin React React React Twig Twig React By sharing store they can share state Twig Share store between components
  114. 114. Things to consider
  115. 115. Context
  116. 116. Nacho Martín nacho@limenius.com @nacmartin Context variables. PHP [ 'serverSide' => $serverSide, 'href' => $request ->getSchemeAndHttpHost() . $request ->getRequestUri(), 'location' => $request ->getRequestUri(), 'scheme' => $request ->getScheme(), 'host' => $request ->getHost(), 'port' => $request ->getPort(), 'base' => $request ->getBaseUrl(), 'pathname' => $request ->getPathInfo(), 'search' => $request ->getQueryString(), ];
  117. 117. Nacho Martín nacho@limenius.com @nacmartin Context in the JS side export default (initialProps, context) => { if (context.serverSide) { return <StaticRouter basename={context.base} location={context.location} />; } else { return <BrowserRouter basename={context.base} />; } };
  118. 118. Header tags
  119. 119. Nacho Martín nacho@limenius.com @nacmartin Extracting headers react-helmet (vue-helmet) import { Helmet } from "react-helmet"; class Application extends React.Component { render() { return ( <div className="application"> <Helmet> <meta charSet="utf-8" /> <title>My Title </title> <link rel="canonical" href="http: //mysite.com/example" /> </Helmet> ... </div> ); } }
  120. 120. Nacho Martín nacho@limenius.com @nacmartin Extracting headers export default (initialProps, context) => ({ renderedHtml: { componentHtml: renderToString( <StaticRouter basename={context.base} location={context.location} context={{}}> <App initialProps={initialProps} appContext={context} /> </StaticRouter> ), title: Helmet.renderStatic().title.toString() } }); Return array instead of component
  121. 121. Nacho Martín nacho@limenius.com @nacmartin Then in Twig {% set recipes = react_component_array('RecipesApp', {'props': props}) %} {% block title %} {{ recipes.title is defined ? recipes.title | raw : '' }} {% endblock title %} {% block body %} {{ recipes.componentHtml | raw }}{{ redux_store('recipesStore', initialState) }} {% endblock body %}
  122. 122. Make reality checks
  123. 123. Nacho Martín nacho@limenius.com @nacmartin Better to try it soon Certain JS code doesn’t make sense in SSR: • Timers: SetTimeout, setInterval. • Access to window, or document objects. • Access to the DOM. • First render that depends on weird things like API calls (this is a smell). Don’t wait until the last moment to check SSR if it is in the roadmap
  124. 124. Summary: What is SSR and what is it for Ways to do it in PHP (pros & cons) Some libraries Tips & practical problems
  125. 125. Thanks! Questions? Nacho Martín nacho@limenius.com @nacmartin

×