Server-side rendering is a hot topic for client applications. Unfortunately, this is not an easy task, especially if you are not developing in a Node.js environment.
I’ve released two libraries to make PHP render from the server possible: Spatie /server-side-rendering and Spatie /laravel-server-side-rendering for Laravel applications.
Let’s take a closer look at some server-side rendering concepts, weigh the pros and cons, and follow the first rule of building a server-side rendering in PHP.
What is server-side rendering
A single page App (also known as SPA) is a client-side rendering App. This is a browser-only application. If you’re using frameworks like React, vue. js or AngularJS, the client will render your App from scratch.
What the browser does
The browser has to go through several steps before the SPA is up and ready for use.
- Download JavaScript scripts
- Parsing JavaScript scripts
- Running JavaScript scripts
- Fetching data (optional, but common)
- Render application in an otherwise empty container (first meaningful render)
- Ready to go! (Interactive)
The user doesn’t see anything meaningful until the browser has completely rendered the App (which takes a little time). This creates a significant delay until the first meaningful rendering is complete, which affects the user experience.
This is where server-side rendering (commonly known as SSR) comes in. SSR prerenders initial application state on the server. Here are the steps that browsers need to go through after using server-side rendering:
- Render HTML from the server (first meaningful render)
- Download JavaScript scripts
- Parsing JavaScript scripts
- Running JavaScript scripts
- Retrieve the data
- Make existing HTML pages interactive
- Ready to go! (Interactive)
Because the server provides pre-rendered blocks of HTML, the user doesn’t have to wait until everything is done to see meaningful content. Note that while interaction times are still at the bottom, the perceived performance has improved dramatically.
Advantages of server-side rendering
The main benefit of server-side rendering is that it improves the user experience. Also, if your site needs to deal with old crawlers that can’t execute JavaScript, SSR will be a must so that crawlers can index a rendered page on the server instead of an empty document.
How does the server render?
It is important to remember that server-side rendering is not trivial. When your Web application is running in both the browser and the server, and your Web application relies on DOM access, you need to make sure that these calls are not triggered on the server side because there is no DOM API available.
Infrastructure complexity
If you’re reading this, chances are you’re building most of your application using PHP, assuming you decide to server render your application. However, the server-side rendered SPA needs to run in a Node.js environment, so a second program will need to be maintained.
You need to build a bridge between two applications so they can communicate and share data: you need an API. Building stateless apis is harder than building stateful ones. You need to be familiar with new concepts such as JWT or OAUTH based validation, CORS, REST, and it is important to add these to existing applications.
We’ve built SSR to increase the user experience of Web applications, but SSR comes at a cost.
Server-side rendering trade-offs
There is an extra operation on the server. One is the increased load on the server, and the second is the slightly longer page response time. However, since the server is now returning valid content, the second problem doesn’t matter from the user’s perspective.
Most of the time you will use Node.js to render your SPA code. If your back-end code is not written in Javascript, the addition of the Node.js stack will complicate your application architecture.
To simplify the infrastructure, we need to find a way to render the client application using the existing PHP environment as a server.
Render JavaScript in PHP
To render SPA on the server side, you need three things:
- An engine that can execute JavaScript
- A script that can render the application on the server
- A script that can render and run the application on the client side
SSR scripts 101
The following example uses vue.js. If you’re used to using other frameworks like React, don’t worry, the core idea is similar and everything looks similar.
For simplicity, let’s use the classic “Hello World” example.
Here is the code for the program (no SSR) :
// app.js import Vue from 'vue' new Vue({ template: ` <div>Hello, world! </div> `, el: '#app' })Copy the code
This short code instantiates a Vue component and renders it in a container (an empty div with an ID of APP).
If you run this script on the server, it will throw an error because there is no DOM to access and Vue tries to render the application in an element that does not exist.
Refactor the script to run on the server.
// app.js import Vue from 'vue' export default () => new Vue({ template: ` <div>Hello, world! </div> ` }) // entry-client.js import createApp from './app' const app = createApp() app.$mount('#app')Copy the code
We split the previous code into two parts. App.js acts as the factory to create the application instance, while the second part, entry-client.js, runs in the browser, which uses the factory to create the application instance and mounts it in the DOM.
Now we can create an application without DOM dependencies and write a second script for the server.
// entry-server.js
import createApp from './app'
import renderToString from 'vue-server-renderer/basic'
const app = createApp()
renderToString(app, (err, html) => {
if (err) {
throw new Error(err)
}
// Dispatch the HTML string to the client...
})
Copy the code
We introduced the same application factory, but we used server-side rendering to render a pure HTML string that would contain a representation of the initial state of the application.
We already have two of the three key elements in place: server-side scripts and client-side scripts. Now, let’s run it on PHP!
Execute JavaScript
When you run JavaScript in PHP, the first choice that comes to mind is V8Js. V8Js is a V8 engine embedded in PHP extensions that allows us to execute JavaScript.
Executing scripts with V8Js is straightforward. We can capture the results using the output buffer in PHP and print in JavaScript.
$v8 = new V8Js(); ob_start(); $v8->executeString($script); echo ob_get_contents(); print('<div>Hello, world! </div>')Copy the code
The downside of this approach is that it requires third-party PHP extensions, which may be difficult or impossible to install on your system, so if there is an alternative, it is a better choice.
The alternative is to run JavaScript using Node.js. We can start a Node process that runs the script and captures the output. Symfony’s Process component is exactly what we want.
use Symfony\Component\Process\Process; New Process([$nodePath, $scriptPath]); new Process([$nodePath, $scriptPath]); echo $process->mustRun()->getOutput(); console.log('<div>Hello, world! </div>')Copy the code
Note that in Node you call console.log instead of print.
Let’s make it happen!
One of the key concepts in the Spatie/Server-side-Rendering package is the engine interface. An engine is an abstraction of JavaScript execution described above.
namespace Spatie\Ssr; /** * Create engine interface. */ interface Engine { public function run(string $script): string; public function getDispatchHandler(): string; }Copy the code
The run method expects the input of a script (the contents of the script, not a path) and returns the result of execution. GetDispatchHandler allows the engine to declare how it expects the script to present the publication. Examples include the print method in V8, or console.log in Node.
The V8Js engine is not very fancy to implement. It’s more like a validation of our ideas above, with some additional error handling mechanisms.
namespace Spatie\Ssr\Engines; use V8Js; use V8JsException; use Spatie\Ssr\Engine; use Spatie\Ssr\Exceptions\EngineError; /** * Create a V8 class to implement the Engine interface class Engine. */ class V8 implements Engine { /** @var \V8Js */ protected $v8; public function __construct(V8Js $v8) { $this->v8 = $v8; } /** * opens the buffer. * Returns the script processing results of v8 stored in the buffer. */ public function run(string $script): string { try { ob_start(); $this->v8->executeString($script); return ob_get_contents(); } catch (V8JsException $exception) { throw EngineError::withException($exception); } finally { ob_end_clean(); } } public function getDispatchHandler(): string { return 'print'; }}Copy the code
Note here that we rethrow V8JsException as our EngineError. This way we can catch the same exception in any engine line of sight.
Node engines are a little more complicated. Unlike V8Js, Node requires files to execute, not script content. Before a server script can be executed, it needs to be saved to a temporary path.
namespace Spatie\Ssr\Engines; use Spatie\Ssr\Engine; use Spatie\Ssr\Exceptions\EngineError; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\ProcessFailedException; /** * Create a Node class to implement the Engine interface class Engine. */ class Node implements Engine { /** @var string */ protected $nodePath; /** @var string */ protected $tempPath; public function __construct(string $nodePath, string $tempPath) { $this->nodePath = $nodePath; $this->tempPath = $tempPath; } public function run(string $script): string {// generate a random, unique temporary file path. $tempFilePath = $this->createTempFilePath(); // Write the script content to a temporary file. file_put_contents($tempFilePath, $script); // Create temporary file for process execution. $process = new Process([$this->nodePath, $tempFilePath]); try { return substr($process->mustRun()->getOutput(), 0, -1); } catch (ProcessFailedException $exception) { throw EngineError::withException($exception); } finally { unlink($tempFilePath); } } public function getDispatchHandler(): string { return 'console.log'; } protected function createTempFilePath(): string { return $this->tempPath.'/'.md5(time()).'.js'; }}Copy the code
With the exception of the temporary path steps, the implementation approach also seems fairly straightforward.
Now that we’ve created the Engine interface, we need to write the rendering classes. The following rendering classes are from the Spatie/Server-side-Rendering extension pack and are a basic rendering class structure.
The only dependency of the render class is the implementation of the Engine interface:
class Renderer { public function __construct(Engine $engine) { $this->engine = $engine; }}Copy the code
The render method handles the logic of the render part. To execute a JavaScript script file, you need the following two elements:
- Our application script file;
- A distribution method to get the HTML generated by parsing;
A simple render looks like this:
class Renderer
{
public function render(string $entry): string
{
$serverScript = implode(';', [
"var dispatch = {$this->engine->getDispatchHandler()}",
file_get_contents($entry),
]);
return $this->engine->run($serverScript);
}
}
Copy the code
This method takes the path to the entry-server.js file as an argument.
We need to distribute the pre-parsed HTML from the script to the PHP environment. The Dispatch method returns the getDispatchHandler method in the Engine class, and Dispatch needs to be run before the server script loads.
Remember our server-side entry script? Next we call our dispatch method in this script:
// entry-server.js
import app from './app'
import renderToString from 'vue-server-renderer/basic'
renderToString(app, (err, html) => {
if (err) {
throw new Error(err)
}
dispatch(html)
})
Copy the code
Vue’s application script requires no special handling, just reading the file using the file_get_contents method.
We have successfully created an SSR for PHP. The full Renderer in Spatie/Server-side-Rendering is a bit different from our implementation, with higher fault tolerance and richer features such as a mechanism for PHP and JavaScript to share data. If you are interested, I suggest you read the source server-side-rendering code library.
Look before you leap
We figured out the pros and cons of server-side rendering and knew that SSR would add complexity to the application architecture and infrastructure. If server-side rendering doesn’t provide any value to your business, then you probably shouldn’t consider it first.
If you do want to start using server-side rendering, read the application architecture first. Most JavaScript frameworks have an in-depth guide to SSR. Vue. Js even has a dedicated SSR documentation site that explains pokholes such as data capture and managing applications for server-side rendering.
If possible, use battle-tested solutions
There are many battle-tested solutions that provide a great SSR development experience. For example, if you’re building a React app, use Next. Js, or if you prefer Vue, use nuxt.js. These are eye-catching projects.
Is not enough? Try PHP server rendering
You can manage the complexity of your infrastructure with limited resources. You want server-side rendering as part of a large PHP application. You don’t want to build and maintain stateless apis. If these reasons are in your case, then server-side rendering using PHP is a good idea.
I have published two libraries to support server-side JavaScript rendering in PHP: Spatie/Laravel-Server-side-rendering and Specially designed Laravel apps. The Laravel custom edition is available in almost zero configuration for Laravel applications, while the general edition requires some adjustments based on the operating environment. Of course, you can refer to the package readme for details.
If you just want to experiment, check out the project from Spatie/Laravel-server-side-rendering-examples and refer to the guide for installation.
If you are considering server-side rendering, I hope this kind of package will help you and look forward to further questions and feedback via Github!
For more on modern PHP, go to
Laravel/PHP knowledge community