multi pass rendering - PhpGt/WebEngine GitHub Wiki
Web application performance is an extremely important metric to take into account when picking tooling and techniques. Certain decisions should be made during the planning phase of an application to ensure that decisions are conscious of the problems they are trying to solve, keeping in mind the simplicity and maintainability of the source code.
Multi-pass rendering is a technique that provides developers three main benefits:
- Develop web applications with the ease and familiarity of writing PHP, using a typical request-response lifecycle.
- Build fully functioned, fully tested applications before adding any extra logic to enhance performance.
- Obtain the fastest possible time to first byte - a dynamic PHP application can serve pages just as fast as a static site can when using multi-pass rendering.
"What are we trying to achieve, and why?" Asking these two questions at the start of a project provides us with all of the solutions that are required in any technical system. However, failing to ask them will lead to the implementation of many irreversible choices.
Without introducing any special techniques, PHP applications produce web pages by performing all calculations to produce the page's content before rendering any response to the browser. This introduces a slight delay on the rendering of the web page. In fact, as soon as any server-side language such as PHP is used to output a page, we slightly delay the output of the page, even when the page's content doesn't often change.
Introducing multi-pass rendering to our application, along with a few webserver tricks, can produce applications with unparalleled response times. In this section, we'll discuss the considerations we need to make at the start of an application's technical design, and follow through to show a fully working, non-trivial example application that can be used as a reference point for producing your own applications, or directly comparing other performance techniques.
A web site or web application consists of multiple web pages; the definition of a web page is the whole HTML document that is sent to the browser, in response to the original request. Requests are made by browsers to URLs, so we can assume a web page is the outcome of visiting a URL.
A typical PHP application has the following lifecycle:
- The browser makes a request to a URL on the server.
- The server passes the request to PHP.
- PHP processes the request to create a response.
- The response is rendered back to the browser as HTML.
For complex applications, point 3 above is where all the time is spent connecting to databases, interrogating data and reacting to user input. All of these operations have to be performed before the browser gets any response. When the browser is waiting for a response, this produces bad user experience.
Performing all processing in one operation, or one "pass", is described above, but it is possible to break off the slow operations of the application and perform them after we have given the user some sort of response.
Multi-pass rendering (MPR) can look like this:
- The browser makes a request to a URL on the server.
- The server responds instantly with a pre-rendered version of the page.
- The browser makes a MPR request to the same URL on the server.
- The server passes the MPR request to PHP.
- PHP processes the request to create a response.
- The response is rendered back to the browser.
- The browser updates the areas on the page that have dynamic content.
This request-response flow defers the slow operations to be performed after the user has received something. What exactly the user receives as the first pass is down to the design of the application, but typically this would consist of a static web page with placeholders for dynamic data.
Depending on what operations are being deferred, there could be multiple calls to point 3 above. Different areas of the page can be processed asynchronously, hence why this technique is called "multi" pass rendering, and not "two" pass rendering.
// TODO.
Read more in the Distinct logic authority section.
When developing any application, it's important to understand and respect the web stack that we are building on. It's even more important to be able to get to a fully working application that can be fully tested, before introducing any new concepts that enhance speed or polish of the application.
The first step is to determine the data's state. On the web, we have the privilege to have the concept of URLs to describe some of the application's state. URLs should be seen as the entrypoint to each state of an application. From one particular URL, it should be clear what elements of the response require dynamic content.
Once the dynamic areas of a response have been identified, we can made the informed decisions to isolate their functionality, ready for implementing MPR.
WebEngine promotes a static-first approach to developing pages. It is required to have the view of the page in an HTML page first, with the logic of the page in its own PHP file. See Page View and Page Logic for more information.
Once the static HTML is made, and we know where in the HTML represents dynamic content, we can develop the application's logic in PHP as normal. The entry point of Page Logic is the go()
function. Rather than containing any logic itself, the go function should be seen as a way to dispatch our code to different areas for dynamic processing. The simplest way to do this is create a separate function for each operation in the page. For example, the go function could look like this:
function go() {
$this->outputSocialMedia(
$this->document->getElementById("social-links")
);
$this->handleUserInput();
$this->outputData(
$this->document->forms,
$this->database->querySelector("customer")
);
}
Notice how the go function doesn't perform any operations itself and is instead an abstraction of the various operations on the page. This simple distinction means that it is now trivial to later add logic to partially render the page, once we have tested the application is working fully.
Behat and PHPUnit tests should be written to test the functionality of the application so that it becomes obvious if any functionality is broken/missing after enabling MPR. Behat tests are especially useful for catching regressions in functionality.
Once testing has completed and functionality of the application is signed off, we can add the MPR functionality. We could break each function into its own separate pass, but for this example we will just render the dynamic content in a second pass.
We configure PHP to not render any dynamic logic without a multi-pass rendering header present. This can be done by adding the following to the go function:
function go() {
// The outputSocialMedia function does not affect state - it is the same for all page renders.
$this->outputSocialMedia(
$this->document->getElementById("social-links")
);
// Only the following two functions produce dynamic content, so we can choose to only
// execute them when the X-MPR header is present.
if($this->headers->contains("X-MPR")) {
$this->handleUserInput();
$this->outputData(
$this->document->forms,
$this->database->querySelector("customer")
);
}
}
Requesting the page is now a lot faster, because the dynamic content is not rendered to the page on the first pass. We can now use JavaScript to perform the second pass. There are many ways this can be achieved, but here is an example in a nutshell:
// Fetch the current page again, but this time with the X-MPR header:
fetch(location.href, {
headers: {
X-MPR: (new Date()).getTime()
}
}).then(function(response) {
if(response.ok) {
return response.text();
}
}).then(completeMPR);
// The completeMPR function will receive the HTML
// of the full page. We can then use the DOM to swap
// out the elements that have changed.
function completeMPR(html) {
// ... out of scope for this howto ...
// See the example application for a full working example.
}
// Configure web server to serve pages statically.
// Add loading indicators (only after threshold of time is hit, due to already fast speeds). // Perception to user is instant loading. // Perception to browser is close-to-zero time-to-first-byte.
// TODO.