WordPress.org

Make WordPress Core

Changes between Version 7 and Version 22 of Ticket #46370


Ignore:
Timestamp:
12/13/2019 11:49:52 AM (6 months ago)
Author:
jonoaldersonwp
Comment:

Legend:

Unmodified
Added
Removed
Modified
  • Ticket #46370

    • Property Focuses privacy added
  • Ticket #46370 – Description

    v7 v22  
    22Loading custom fonts in a sub-optimal manner can cause significant performance and privacy issues. Anecdotally, inefficient font loading is frequently one of the biggest performance bottlenecks in many WordPress themes/sites.
    33
    4 This is, in part, because there hasn’t been a ‘best practice’ approach; there’s no ‘standard’ way of adding a font to a theme, and ensuring that it’s loaded in a performance-friendly manner.
    5 
    6 Loading resources from Google Fonts has been the generally-accepted workaround to this, but that’s come with privacy concerns, and, poor implementations (no DNS prefetching, multiple requests, loading unneeded localisations/weights, etc).
    7 
    8 Even our own sites and setups ([https://wordpress.org/ wordpress.org], the WordPress admin area, Gutenberg, and some of our [https://wp-themes.com/twentyseventeen/ core themes]) fall afoul of these issues.
    9 
    10 If we’re serious about WordPress becoming a fast platform, we can’t rely on theme developers to add and manage fonts without providing a framework to support them.
     4This is, in part, because there hasn’t been a ‘best practice’ approach; there’s no ‘standard’ way of adding a font to a theme and ensuring that it’s loaded in a performance- and privacy-friendly manner.
     5
     6In almost all cases, theme/plugin developers either enqueue a third-party stylesheet (e.g., Google Fonts), or, bundle and enqueue the CSS for a local font. Both approaches cause problems. In particular:
     7
     8- Loading third-party resources can raise privacy concerns.
     9
     10- Loading fonts from external sources means that WordPress is often unable to optimize those fonts (no DNS prefetching, no intelligent browser prioritisation, no de-duplication, etc).
     11
     12- Bundling/enqueuing local fonts puts a strong reliance on the theme/plugin author understanding the complex nuances of efficient font-loading.
     13
     14Even our own sites and setups ([https://wordpress.org/ wordpress.org], the WordPress admin area, Gutenberg, and some of our [https://wp-themes.com/twentyseventeen/ core themes]) fall afoul of these issues. They're slow, and they have privacy problems.
     15
     16If we’re serious about WordPress becoming a fast, privacy-friendly platform, we can’t rely on theme developers to add and manage fonts without providing a framework to support them.
    1117
    1218== Why now?
     
    1622- Best practices for defining and loading fonts via CSS are now well-established, stable, and see broad usage elsewhere.
    1723
    18 - The increasing adoption of of HTTP/2 means that localising assets can (in many cases) be faster than loading them from remote sources.
    19 
    20 - Specifically, [https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization Google's Web Fundamentals documentation] (which much of this spec is directly lifted and adapted from) describes a stable, compatible, and plug-and-play approach to loading fonts.
     24- The increasing adoption of HTTP/2 means that localising assets can (in many cases) be faster than loading them from remote sources.
     25
     26- There is increasing discomfort with WordPress 'passively endorsing' Google Fonts, which 'leaks' private information (IP addresses).
     27
     28Now, standards like [https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization Google's Web Fundamentals documentation] (which much of this spec is directly lifted and adapted from) describe a stable, compatible, and plug-and-play approach to loading fonts.
     29
     30There are well-defied, standardised approaches to loading fonts, which we can implement into WordPress core. This will allow theme and plugin developers to bypass all of these pitfalls.
    2131
    2232== The vision
     
    2737A move to an enqueue-based approach may also provide plugin developers with hooks to intercept the default behaviours, and to modify the output. E.g., to utilise the [https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization#the_font_loading_api font loading API] rather than outputting CSS. This provides a huge opportunity for caching and performance-optimising plugins to speed up WordPress sites/themes.
    2838
     39In its simplest form, `wp_enqueue_font()` provides a thin abstraction over existing WP mechanisms (`wp_enqueue_style()`), `wp_add_inline_style()`, etc), and simply enforces a performant, privacy-friendly implementation.
     40
    2941= The ‘Web Fundamentals’ approach
    3042The [https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/webfont-optimization Web Fundamentals documentation] provides an example of an optimal CSS structure for loading fonts. E.g.,
     
    3547  font-style: normal;
    3648  font-weight: 400;
     49  font-display: auto;
    3750  src: local('Awesome Font'),
    3851       url('/fonts/awesome-l.woff2') format('woff2'),
     
    4053       url('/fonts/awesome-l.ttf') format('truetype'),
    4154       url('/fonts/awesome-l.eot') format('embedded-opentype');
    42   unicode-range: U+000-5FF; /* Latin glyphs */
     55  unicode-range: U+000-5FF;
    4356}
    4457
     
    4760  font-style: normal;
    4861  font-weight: 700;
     62  font-display: auto;
    4963  src: local('Awesome Font'),
    5064       url('/fonts/awesome-l-700.woff2') format('woff2'),
     
    5266       url('/fonts/awesome-l-700.ttf') format('truetype'),
    5367       url('/fonts/awesome-l-700.eot') format('embedded-opentype');
    54   unicode-range: U+000-5FF; /* Latin glyphs */
     68  unicode-range: U+000-5FF;
    5569}
    5670}}}
     
    6074In addition to using an optimal CSS approach, the documentation recommends using a `<link rel="preload">` directive (or equivalents JavaScript-based approaches). This prevents the browser from having to wait until the render tree is complete before downloading font resources.
    6175
    62 Our aim is to enable all themes/plugins to achieve this approach and to output this code (the CSS, paired with a preload directive) without requiring developers to explicitly craft and maintain this code.
     76Our aim is to enable all themes/plugins to achieve this type of approach and to output this approach (the optimized CSS, paired with a preload directive) without requiring developers to explicitly craft and maintain this code.
    6377
    6478= A spec
     
    7286 * Enqueue font.
    7387 *
    74  * @param string            $handle The name of the font.
     88 * @param string            $family The name of the font family.
    7589 * @param string|array|bool $src    The source(s) of the font files.
    7690 * @param array $params {
    7791 *      Params.
    7892 *
    79  *      @type string      $style     The style of the font.
    80  *      @type int         $weight    The weight of the font.
    81  *      @type string      $display   Display/swap behaviour.
    82  *      @type string      $range     A unicode range value.
    83  *      @type string|bool $external  Externalise the CSS file/code.
    84  *      @type bool        $preload   Should the resource be preloaded.
    85  *      @type bool        $in_footer Output the CSS/file in the footer.
     93 *      @type string        $style     The style of the font.
     94 *      @type int           $weight    The weight of the font.
     95 *      @type string        $display   Display/swap behaviour.
     96 *      @type string|array  $variation Variation settings.
     97 *      @type string        $range     A unicode range value.
     98 *      @type string|bool   $external  Externalise the CSS file/code.
     99 *      @type bool          $preload   Should the resource be preloaded.
     100 *      @type bool          $in_footer Output the CSS/file in the footer.
     101 *      @type string|bool   $media     Apply a media query to the output.
    86102 * }
    87103 */
    88 function wp_enqueue_font( $handle, $src = false, $params = array() ) {
     104function wp_enqueue_font( $family, $src = false, $params = array() ) {
    89105        $params = wp_parse_args(
    90106                $params,
    91107                array(
    92                         'style'     => 'normal',
    93                         'weight'    => 400,
    94                         'display'   => 'auto',
    95                         'range'     => false,
    96                         'external'  => false,
    97                         'preload'   => true,
    98                         'in_footer' => false,
     108                        'style'                   => 'normal',
     109                        'weight'                  => 400,
     110                        'display'                 => 'auto',
     111                        'font-variation-settings' => normal,
     112                        'range'                   => false,
     113                        'external'                => false,
     114                        'preload'                 => true,
     115                        'in_footer'               => false,
     116                        'media'                   => false
    99117                )
    100118        );
     
    103121}}}
    104122
    105 Note that the args after `$src` are in an associative array to avoid having to supply positional params with default values and to make it easier to easily identify the parameters. Something similar could be done for `wp_register_script()` and `wp_register_style()`.
     123Note that the args after `$src` are in an associative array to avoid having to supply positional params with default values and to make it easier to easily identify the parameters. It's worth considering that something similar could be done for `wp_register_script()` and `wp_register_style()`.
    106124
    107125== Starting from simplicity
     
    111129<?php
    112130wp_enqueue_font(
    113   'Awesome Font', 
     131  'Awesome Font',
    114132  '/fonts/awesome-font-400.woff2'
    115133);
    116134}}}
    117135
    118 In this example, we’ve only specified the bare minimum information required to enqueue a font - a name and a location. But there’s enough here that we can generate the following CSS:
     136In this example, we’ve specified the bare minimum information required to enqueue a font - a family and a location. But there’s enough here that we can generate the following CSS:
    119137
    120138{{{#!css
     
    128146}}}
    129147
    130 - ''NOTE: If no `$src` value is defined, only a local font should be referenced (via `$handle`).''
    131 
    132 == Supporting font registration
    133 As with scripts and styles, we should allow for registration and enqueuing as separate  processes. I.e., `wp_register_font` should accept the same arguments as `wp_enqueue_font`, and then, `wp_enqueue_font` can simply reference a registered `$handle`.
     148We can simplify even further, though. If no `$src` value is defined, then a local src can still be referenced via the `$family`. Note that this will only work if the user's system has the font installed locally. E.g.:
     149
     150{{{#!php
     151wp_enqueue_font('Awesome Font');
     152}}}
     153
     154Produces the following:
     155
     156{{{#!css
     157@font-face {
     158  font-family: 'Awesome Font';
     159  font-style: normal;
     160  font-weight: 400;
     161  src: local('Awesome Font');
     162}
     163}}}
     164
     165== Registering and Enqueue'ing
     166As with scripts and styles, we should allow for registration and enqueuing as separate processes.
     167
     168I.e., `wp_register_font()` should accept the same parameters as `wp_enqueue_font()`. Filters/hooks should be introduced to enable theme/plugin authors to manipulate the behaviour of fonts between these stages.
     169
     170Behind the scenes, there's some additional complexity here. Scripts and styles use a `$handle` to identify them uniquely. Fonts don't have an equivalent concept (multiple font variations may share the same family namespace), so we need to synthesize one. This will allow us to pass gracefully wrap functions like `wp_enqueue_style()`.
     171
     172To achieve this, we should combine the (sanitized) `$family`, `$style`, `$weight` and `$media` strings to create a unique representation; e.g., `base64encode($family.$style.$weight.$media)`.
     173
     174If multiple fonts are registered with the same handle, the last-enqueued version should take priority (overwriting previous enqueues).
     175
     176=== De-regestering and de-queue'ing
     177
     178As we've highlighted, the `$handle` is sometimes insufficient to represent a unique font (for the purposes of identification, registration, and conflict  management).
     179
     180This means that `wp_dequeue_font()` and `wp_deregister_font()` should accept an optional array of values in addition to the handle. E.g.,
     181
     182{{{#!php
     183/**
     184 * Dequeue font.
     185 *
     186 * @param string            $family The family of the font.
     187 * @param array|false       $params {
     188 *      Params.
     189 *
     190 *      @type string        $style     The style of the font.
     191 *      @type string        $weight    The weight of the font.
     192 *      @type string        $media     The media query for the font.
     193 * }
     194*/
     195}}}
     196
     197If only a string is passed, ''all'' fonts matching that `$family` name should be removed. If an array is passed, then only fonts matching the family ''and'' the passed params should be dequeued/deregistered.
     198
    134199
    135200== Definitions
     201Some of the properties we're using here are lifted directly from `wp_enqueue_style()`, and should be treated identically and passed directly through to that function (e.g., `$in_footer`, `$media`). Some are fairly self-descriptive, and shouldn't need any special consideration (e.g., `$range`).
     202
     203The unique and more complex remaining properties are explored below.
     204
    136205=== $src
    137206Whilst our `$src` variable can accept a simple string, more advanced usage should specify multiple versions and their formats. That looks like this (maintaining the rest of our ‘simplest implementation’ approach):
     
    140209<?php
    141210wp_enqueue_font(
    142   'Awesome Font', 
     211  'Awesome Font',
    143212  array(
    144213    'woff2'             => '/fonts/awesome-font-400.woff2',
     
    154223- ''NOTE: Data URLs (e.g., `data:application/font-woff2;charset=utf-8;base64[...]`) may be provided instead of file paths.''
    155224
     225The source values may be either a local or absolute URL (including remote URLs). However, this spec assumes (and prefers) that plugins and themes should generally store their font files locally; e.g., `/wp-content/themes/{{theme_name}}/fonts/` or `/wp-content/plugins/{{plugin_name}}/fonts/`.
     226
     227=== $variation
     228There's a maturing standard around 'variable fonts', which adds a 'font-variation-settings' property. This accepts a string of key/value pairs, which define attributes like the font's weight, slant, or other variations.
     229
     230The `$variation` property accepts either a string (`normal`), or, an array of key/value pairs (e.g., `["wght" => 637, "wdth" => 100]`), and returns a string of these values (e.g., `wght 637, wdth 100`).
     231
    156232=== $external
    157233When the `$external` flag is false (which is the default behaviour), the generated CSS should be output in the <head>, via `wp_add_inline_style()` (unless `$in_footer` is set to `true`, in which case, the code should be output in a generated `<style>` tag hooked into `wp_footer`).
    158234
    159 When set to `true`, we should wrap `wp_enqueue_style` and call a procedurally generated CSS file containing the relevant CSS (with a filename of `font-$handle-$style-$weight.css`).
    160 
    161 Alternatively, the value may be a string which represents a filepath, where the CSS ‘file’ should be accessible from (via a rewrite rule).
    162 
    163 - ''NOTE: When `$external` is true, we should check that the composite handle for the font can be converted to a unique stylesheet handle. If such a stylesheet handle already exists, we should de-enqueue the existing stylesheet and enqueue the new one.''
    164 - ''NOTE: Default location of generated CSS file TBD.''
    165 - ''NOTE: Invalid string values should fall back to the default behaviour.''
     235When `$external` is set to `true`, we should use `wp_register_style()` and `wp_enqueue_style()` to reference a procedurally generated CSS file containing the relevant CSS at `/wp-content/fonts/$handle.css`.
     236
     237Because we can't rely on having write permission to that folder (or know that it even exists), we should _not_ try to create a physical file, but rather, utilise a URL rewrite similar to the approach used in `do_robots()` to serve `/robots.txt`.
     238
     239Note that this assumes the standardisation of /wp-content/fonts/ as a protected space. New WordPress installations should generate this (empty) folder.
     240
     241When `$external` is set to a string, we should use the string value as the endpoint to reference (e.g., `/wp-content/plugins/{{some-plugin}}/fonts/custom-endpoint.css`).
    166242
    167243=== $preload
    168 When the `$preload` flag is true (which is the default behaviour), `wp_resource_hints`* should be filtered to add a `<link rel="preload">` directive for the most modern format font file in the $src array (usually woff2). 
     244When the `$preload` flag is true (which is the default behaviour), `wp_resource_hints`* should be filtered to add a `<link rel="preload">` directive for the most modern format font file in the $src array (usually woff2).
    169245
    170246If `$external` is set to true, the associated CSS file should also be preloaded.
     
    172248- ''NOTE: The preload tag is currently unsupported by `wp_resource_hints`, and it's expected that another (similar) function may be introduced to support this. Discussion [https://core.trac.wordpress.org/ticket/42438 here].''
    173249
    174 = Other considerations
    175 == Unique handles
    176 Unlike a style/script, the `$handle` is insufficient to represent a unique font (for the purposes of identification, registration, and conflict management). For fonts, we should consider the combination of the `$handle`, `$style` and `$weight` to represent a unique instance.
    177 
    178 - ''NOTE: If there are non-unique instances of a font, the last-enqueued version should take priority.''
     250= Other considerations
    179251
    180252== Invalid values & minimum requirements
     
    183255Invalid or malformed values for parameters without constrained values (e.g., `$range`, `$src`) should be ignored.
    184256
    185 If this validation results in there being no values for `$handle` and `$src` (which represents the bare minimum requirements), no CSS should be generated/output.
     257If this validation results in there being no values for `$family` and `$src` (which represents the bare minimum requirements), no CSS should be generated/output.
     258
     259= Hooks & filters
     260
     261Filters/hooks should be introduced to enable theme/plugin authors to manipulate the behaviour of fonts between each of the stages we've defined - registration, enqueuing, and output (in various formats/locations).
     262
     263These will need to be defined.
    186264
    187265= Next steps
    188 There are lots of moving parts on this one, but I’m hoping that most of it is fairly straightforward. I’d love some feedback on (any gaps in / issues with) the spec. 
     266There are lots of moving parts on this one, but I’m hoping that most of it is fairly straightforward. I’d love some feedback on (any gaps in / issues with) the spec.
    189267
    190268I’m anticipating that we'll need a bunch of discussion, iteration, exploration and definition before it makes sense to start authoring any code on this one, but that said, it’d be super to see some of this start to take shape.