Skip to content

Add pre/post output buffering filters for AMP HTML caching #8075

@Michelleeby

Description

@Michelleeby

Hello folks! I am a Software Engineer with WordPress VIP at Automattic. Some of my work involves load testing applications for performance analysis. For customers we see using AMP, we often see AMP_Theme_Support::finish_output_buffering and AMP_Content_Sanitizer::sanitize_document show up in slow traces during load tests. For platforms with persistent object cache, there is huge potential for performance gains by caching the output and short circuiting. This would eliminate many repeat and expensive calls on subsequent requests. For customers with scale and high traffic in mind, this is important to application stability.

This suggestion adds pre_amp_finish_output_buffering and amp_finish_output_buffering filters near finish_output_buffering in order to enable performant page caching for AMP HTML outputs via object cache or other means.

Filter usage:

pre_amp_finish_output_buffering: return cached value if present to short-circuit buffer.
amp_finish_output_buffering: save freshly prepared output after processing.

    /**
		 * Filter to allow lookup of a cached AMP output before expensive processing.
		 *
		 * Returning a non-false value will short-circuit AMP output processing.
		 *
		 * @since X.X.X
		 *
		 * @param false|string $cached_response Cached value, or false to proceed.
		 * @param string       $buffered_output Initial output buffer.
		 */
		$cached = apply_filters( 'pre_amp_finish_output_buffering', false, $response );
		if ( false !== $cached && is_string( $cached ) ) {
			return $cached;
		}
		/**
		 * Filter to allow saving or processing the AMP output buffer after it's rendered.
		 *
		 * Use this to cache the response, for example.
		 *
		 * @since X.X.X
		 *
		 * @param string $response Final processed AMP response (HTML).
		 * @param string $buffered_output Initial output buffer before processing.
		 */
		$response = apply_filters( 'amp_finish_output_buffering', $response, $response /* NOTE: pass original if needed */ );

These filters would then allow a user to leverage caching like this:

/**
 * Example: Cache the *full AMP output* by document URL in object cache (for 2 minutes).
 */
add_filter( 'pre_amp_finish_output_buffering', function( $false, $response ) {
    $key = 'wpvip_amp_output_' . md5( amp_get_current_url() );
    $cached = wp_cache_get( $key, 'amp_output' );
    return ( false === $cached ) ? false : $cached;
}, 10, 2 );

add_filter( 'amp_finish_output_buffering', function( $response, $original ) {
    $key = 'wpvip_amp_output_' . md5( amp_get_current_url() );
    wp_cache_set( $key, $response, 'amp_output', 120 );
    return $response;
}, 10, 2 );

We've seen this reduce response times on AMP endpoints by ~25% in testing.

plugins/amp/includes/class-amp-theme-support.php:

    /**
	 * Finish output buffering.
	 *
	 * @since 0.7
	 * @see AMP_Theme_Support::start_output_buffering()
	 *
	 * @param string $response Buffered Response.
	 * @return string Processed Response.
	 */
	public static function finish_output_buffering( $response ) {
		self::$is_output_buffering = false;

		/**
		 * Filter to allow lookup of a cached AMP output before expensive processing.
		 *
		 * Returning a non-false value will short-circuit AMP output processing.
		 *
		 * @since X.X.X
		 *
		 * @param false|string $cached_response Cached value, or false to proceed.
		 * @param string       $buffered_output Initial output buffer.
		 */
		$cached = apply_filters( 'pre_amp_finish_output_buffering', false, $response );
		if ( false !== $cached && is_string( $cached ) ) {
			return $cached;
		}

		try {
			$response = self::prepare_response( $response );
		} catch ( Error $error ) { // Only PHP 7+.
			$response = self::render_error_page( $error );
		} catch ( Exception $exception ) {
			$response = self::render_error_page( $exception );
		}

		/**
		 * Filter to allow saving or processing the AMP output buffer after it's rendered.
		 *
		 * Use this to cache the response, for example.
		 *
		 * @since X.X.X
		 *
		 * @param string $response Final processed AMP response (HTML).
		 * @param string $buffered_output Initial output buffer before processing.
		 */
		$response = apply_filters( 'amp_finish_output_buffering', $response, $response /* NOTE: pass original if needed */ );

		/**
		 * Fires when server timings should be sent.
		 *
		 * This is immediately before the processed output buffer is sent to the client.
		 *
		 * @since 2.0
		 * @internal
		 */
		do_action( 'amp_server_timing_send' );
		return $response;
	}

Thanks so much for your time! Looking forward to connecting on this 😁

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions