Make WordPress Core


Ignore:
Timestamp:
10/15/2025 05:12:32 PM (8 weeks ago)
Author:
westonruter
Message:

General: Introduce output buffering for template enhancements.

This introduces an output buffer for the entire template rendering process. This allows for post-processing of the complete HTML output via filtering before it is sent to the browser. This is primarily intended for performance optimizations and other progressive enhancements. Extenders must not rely on output buffer processing for critical content and functionality since a site may opt out of output buffering for the sake of streaming. Extenders are heavily encouraged to use the HTML API as opposed to using regular expressions in output buffer filters.

  • A new wp_before_include_template action is introduced, which fires immediately before the template file is included. This is useful on its own, as it avoids the need to misuse template_include filter to run logic right before the template is loaded (e.g. sending a Server-Timing header).
  • The wp_start_template_enhancement_output_buffer() function is hooked to this new action. It starts an output buffer, but only if there are wp_template_enhancement_output_buffer filters present, or else if there is an explicit opt-in via the wp_should_output_buffer_template_for_enhancement filter.
  • The wp_finalize_template_enhancement_output_buffer() function serves as the output buffer callback. It applies wp_template_enhancement_output_buffer filters to the buffered content if the response is identified as HTML.
  • The output buffer callback passes through (without filtering) any content for non-HTML responses, identified by the Content-Type response header.
  • This provides a standardized way for plugins (and core) to perform optimizations, such as removing unused CSS, without each opening their own ad hoc output buffer.

Developed in https://github.com/WordPress/wordpress-develop/pull/8412.

Props westonruter, nextendweb, dmsnell, flixos90, jorbin, peterwilsoncc, swissspidy, DrewAPicture, DaanvandenBergh, OptimizingMatters, tabrisrp, jonoaldersonwp, SergeyBiryukov.
Fixes #43258.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/template.php

    r57685 r60936  
    824824    do_action( 'wp_after_load_template', $_template_file, $load_once, $args );
    825825}
     826
     827/**
     828 * Checks whether the template should be output buffered for enhancement.
     829 *
     830 * By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been
     831 * added be the time a template is included at the {@see 'wp_before_include_template'} action. This allows template
     832 * responses to be streamed as much as possible when no template enhancements are registered to apply.
     833 *
     834 * @since 6.9.0
     835 *
     836 * @return bool Whether the template should be output-buffered for enhancement.
     837 */
     838function wp_should_output_buffer_template_for_enhancement(): bool {
     839    /**
     840     * Filters whether the template should be output-buffered for enhancement.
     841     *
     842     * By default, an output buffer is only started if a {@see 'wp_template_enhancement_output_buffer'} filter has been
     843     * added. For this default to apply, a filter must be added by the time the template is included at the
     844     * {@see 'wp_before_include_template'} action. This allows template responses to be streamed as much as possible
     845     * when no template enhancements are registered to apply. This filter allows a site to opt in to adding such
     846     * template enhancement filters during the rendering of the template.
     847     *
     848     * @since 6.9.0
     849     *
     850     * @param bool $use_output_buffer Whether an output buffer is started.
     851     */
     852    return (bool) apply_filters( 'wp_should_output_buffer_template_for_enhancement', has_filter( 'wp_template_enhancement_output_buffer' ) );
     853}
     854
     855/**
     856 * Starts the template enhancement output buffer.
     857 *
     858 * This function is called immediately before the template is included.
     859 *
     860 * @since 6.9.0
     861 *
     862 * @return bool Whether the output buffer successfully started.
     863 */
     864function wp_start_template_enhancement_output_buffer(): bool {
     865    if ( ! wp_should_output_buffer_template_for_enhancement() ) {
     866        return false;
     867    }
     868
     869    $started = ob_start(
     870        'wp_finalize_template_enhancement_output_buffer',
     871        0, // Unlimited buffer size so that entire output is passed to the filter.
     872        /*
     873         * Instead of the default PHP_OUTPUT_HANDLER_STDFLAGS (cleanable, flushable, and removable) being used for
     874         * flags, the PHP_OUTPUT_HANDLER_FLUSHABLE flag must be omitted. If the buffer were flushable, then each time
     875         * that ob_flush() is called, a fragment of the output would be sent into the output buffer callback. This
     876         * output buffer is intended to capture the entire response for processing, as indicated by the chunk size of 0.
     877         * So the buffer does not allow flushing to ensure the entire buffer can be processed, such as for optimizing an
     878         * entire HTML document, where markup in the HEAD may need to be adjusted based on markup that appears late in
     879         * the BODY.
     880         *
     881         * If this ends up being problematic, then PHP_OUTPUT_HANDLER_FLUSHABLE could be added to the $flags and the
     882         * output buffer callback could check if the phase is PHP_OUTPUT_HANDLER_FLUSH and abort any subsequent
     883         * processing while also emitting a _doing_it_wrong().
     884         *
     885         * The output buffer needs to be removable because WordPress calls wp_ob_end_flush_all() and then calls
     886         * wp_cache_close(). If the buffers are not all flushed before wp_cache_close() is closed, then some output buffer
     887         * handlers (e.g. for caching plugins) may fail to be able to store the page output in the object cache.
     888         * See <https://github.com/WordPress/performance/pull/1317#issuecomment-2271955356>.
     889         */
     890        PHP_OUTPUT_HANDLER_STDFLAGS ^ PHP_OUTPUT_HANDLER_FLUSHABLE
     891    );
     892
     893    if ( $started ) {
     894        /**
     895         * Fires when the template enhancement output buffer has started.
     896         *
     897         * @since 6.9.0
     898         */
     899        do_action( 'wp_template_enhancement_output_buffer_started' );
     900    }
     901
     902    return $started;
     903}
     904
     905/**
     906 * Finalizes the template enhancement output buffer.
     907 *
     908 * Checks to see if the output buffer is complete and contains HTML. If so, runs the content through
     909 * the `wp_template_enhancement_output_buffer` filter.  If not, the original content is returned.
     910 *
     911 * @since 6.9.0
     912 *
     913 * @see wp_start_template_enhancement_output_buffer()
     914 *
     915 * @param string $output Output buffer.
     916 * @param int    $phase  Phase.
     917 * @return string Finalized output buffer.
     918 */
     919function wp_finalize_template_enhancement_output_buffer( string $output, int $phase ): string {
     920    // When the output is being cleaned (e.g. pending template is replaced with error page), do not send it through the filter.
     921    if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) !== 0 ) {
     922        return $output;
     923    }
     924
     925    // Detect if the response is an HTML content type.
     926    $is_html_content_type = null;
     927    $html_content_types   = array( 'text/html', 'application/xhtml+xml' );
     928    foreach ( headers_list() as $header ) {
     929        $header_parts = preg_split( '/\s*[:;]\s*/', strtolower( $header ) );
     930        if (
     931            is_array( $header_parts ) &&
     932            count( $header_parts ) >= 2 &&
     933            'content-type' === $header_parts[0]
     934        ) {
     935            $is_html_content_type = in_array( $header_parts[1], $html_content_types, true );
     936            break; // PHP only sends the first Content-Type header in the list.
     937        }
     938    }
     939    if ( null === $is_html_content_type ) {
     940        $is_html_content_type = in_array( ini_get( 'default_mimetype' ), $html_content_types, true );
     941    }
     942
     943    // If the content type is not HTML, short-circuit since it is not relevant for enhancement.
     944    if ( ! $is_html_content_type ) {
     945        return $output;
     946    }
     947
     948    $filtered_output = $output;
     949
     950    /**
     951     * Filters the template enhancement output buffer prior to sending to the client.
     952     *
     953     * This filter only applies the HTML output of an included template. This filter is a progressive enhancement
     954     * intended for applications such as optimizing markup to improve frontend page load performance. Sites must not
     955     * depend on this filter applying since they may opt to stream the responses instead. Callbacks for this filter are
     956     * highly discouraged from using regular expressions to do any kind of replacement on the output. Use the HTML API
     957     * (either `WP_HTML_Tag_Processor` or `WP_HTML_Processor`), or else use {@see DOM\HtmlDocument} as of PHP 8.4 which
     958     * fully supports HTML5.
     959     *
     960     * @since 6.9.0
     961     *
     962     * @param string $filtered_output HTML template enhancement output buffer.
     963     * @param string $output          Original HTML template output buffer.
     964     */
     965    return (string) apply_filters( 'wp_template_enhancement_output_buffer', $filtered_output, $output );
     966}
Note: See TracChangeset for help on using the changeset viewer.