Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add service worker streaming #85

Merged
merged 25 commits into from
Oct 19, 2018
Merged

Add service worker streaming #85

merged 25 commits into from
Oct 19, 2018

Conversation

westonruter
Copy link
Collaborator

@westonruter westonruter commented Oct 11, 2018

Works best with AMP due to the fact that all scripts in AMP are asynchronous (ampproject/amp-wp#1503). See changes in theme: westonruter/twentyseventeen-westonson#2.

This pull request explores what it would look like to improve performance of navigating a site through the use of streaming. See @jeffposnick's article Beyond SPA for some key inspiration here. The goal is to speed up the perceived performance for navigating a site by showing a meaningful interface and loading indicator immediately. In short, this should reduce the first contentful paint time when navigating an AMP site on origin.

The proposed way that this would work is as follows:

  1. The user opts in to streaming via theme support flag (add_theme_support( 'service_worker_streaming' )) and adds a WP_Service_Worker_Navigation_Routing_Component::do_stream_boundary() call to their theme's header.php. This function outputs a loading message/progressbar to be displayed while the body is loading.
  2. A new query var called wp_stream_fragment is added which can take one of two values: header and body (for example, https://example.com/?wp_stream_fragment=header and https://example.com/?wp_stream_fragment=body). This argument can be added to any URL, and when header is supplied the AMP plugin returns the document up until the do_stream_boundary() call and when body is supplied the document after the do_stream_boundary() call is returned.
  3. The header fragment is precached by the service worker. The title in this header fragment is filtered to say “Loading...”. This header fragment is also served with an inline script containing am wpStreamCombine() function which is called later in the body fragment.
  4. When the service worker handles a navigation request, it will immediately serve the header fragment which will be displayed while the browser is fetching the body fragment. While this is happening the loading indicator is shown.
  5. WordPress serves the body fragment with data extracted from the head children and body attributes which would have normally been included in an entire response. Output buffering is used to capture the header for that given request and it is loaded into a DOMDocument. A JSON representation of this data included in this response and into the wpStreamCombine function (defined in the header fragment) at the very beginning of the body fragment which reconciles the differences between the precached header fragment. In Jeff's example linked above, he used this approach for reconciling the document.title.
  6. If the request to obtain the body fragment results in a redirect, then the resultant URL is set in the user's browser via location.replaceState().
  7. The wpStreamCombine function deletes the element containing loading indicator.
  8. If the body fragment request fails, then a precached offline page body fragment would be served instead. Eventually, the body fragment responses should use a stale-while-revalidate strategy once there is a mechanism to communicate an update back to the user (see amp-install-serviceworker: Trigger event when service worker has been update has been installed ampproject/amphtml#18615 (comment))

The logic for obtaining the elements in the head can be deferred for later so another plugin (like AMP) can obtain the elements after it has sanitized it: ampproject/amp-wp#1503.

Abbreviated example header fragment response

<!DOCTYPE html>
<html lang="en-US" class="no-js" amp="">
	<head>
		<meta charset="UTF-8">
		...
	</head>
	<body class="home">
		...
		<div id="wp-stream-fragment-boundary">Loading...</div>
		<script id="wp-stream-combine-function">
			function wpStreamCombine( data ) { /* ... */ }
		</script>

Abbreviated example body fragment response

	<script id="wp-stream-combine-call">
	wpStreamCombine( {
		"head_nodes": [ /* ... */ ],
		"body_attributes": { /* ... */ }
	} );
	</script>
	<div id="primary" class="content-area">
	...
	</div>
</body>
</html>

Demo

See child theme PR for Twenty Seventeen that implements support: westonruter/twentyseventeen-westonson#2

Video: https://youtu.be/y5J69wLstUM

Live: https://streaming-westonruter.pantheonsite.io/

Todo

  • Given that the header is dynamically-generated by WordPress it becomes very easy for the precached header fragment to become stale. Theme authors may need to add the boundary before the nav menu is output to help mitigate this. Otherwise, a filter like wp_offline_error_precache_entry will be needed with a revision that includes all variability that will be included in the header.
  • When the body request returns in a redirect, we'll need to cause the browser to redirect via a location.replace() call.

@postphotos
Copy link

This is awesome. Really looking forward to this coming together!

redirectedUrl.searchParams.delete( STREAM_HEADER_FRAGMENT_QUERY_VAR );
const script = `<script>history.replaceState( {}, '', ${ JSON.stringify( redirectedUrl.toString() ) } );</script>`;
return response.text().then( ( body ) => {
return new Response( script + body );
Copy link
Collaborator Author

@westonruter westonruter Oct 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeffposnick here's how I ended up dealing with the challenge of handling a streamed body that redirects.

The “Try Redirect” nav menu item points to an old post URL that redirects, so you can see the URL go from "old-post" to "new-post".

Copy link
Collaborator Author

@westonruter westonruter Oct 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

@westonruter westonruter changed the title [WIP] Add service worker streaming Add service worker streaming Oct 16, 2018
$header_html = ob_get_clean();
$libxml_use_errors = libxml_use_internal_errors( true );
$header_html = preg_replace( '#<noscript.+?</noscript>#s', '', $header_html ); // Some libxml versions croak at noscript in head.
$dom = new DOMDocument( $header_html );
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove $header_html as an arg. Not correct.

const canStreamResponse = () => {
const url = new URL( event.request.url );
return ! (
/\.php$/.test( url.pathname ) ||
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing this, why not add *.php files to the blacklist patterns?


const url = new URL( event.request.url );
url.searchParams.append( STREAM_HEADER_FRAGMENT_QUERY_VAR, 'body' );
const request = new Request( url.toString(), {...event.request} );
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify that {...event.request} is working.

.catch( sendOfflineResponse ),
]);

// @todo Handle error case.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done

return response;
}
}
const channel = new BroadcastChannel( 'wordpress-server-errors' );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker at this point, but it would be needed to define the scenario for browsers that do not support BroadcastChannel (https://caniuse.com/#search=broadcastchannel)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is captured in #81.

}
}

const canStreamResponse = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a brief comment here on the need to avoid PHP files. As discussed, this could be moved to the black listing logic.

/**
* Apply the stream body data to the stream header.
*
* @param {Array} data Data.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps expand the expected content of the data argument.

Copy link
Collaborator

@amedina amedina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing work. Progressive Web, the WordPress way. Ship it!

@westonruter westonruter merged commit f647214 into master Oct 19, 2018
@westonruter westonruter deleted the add/sw-streaming branch October 23, 2018 22:24
@westonruter westonruter added this to the 0.2 milestone Apr 16, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants