[wp-trac] [WordPress Trac] #46370: A proposal for creating an API to register and enqueue web fonts
WordPress Trac
noreply at wordpress.org
Tue Mar 5 18:04:54 UTC 2019
#46370: A proposal for creating an API to register and enqueue web fonts
-----------------------------+------------------------------
Reporter: jonoaldersonwp | Owner: (none)
Type: feature request | Status: new
Priority: normal | Milestone: Awaiting Review
Component: General | Version:
Severity: normal | Resolution:
Keywords: | Focuses: performance
-----------------------------+------------------------------
Description changed by westonruter:
Old description:
> = The problem
> Loading 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.
>
> 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.
>
> 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).
>
> 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.
>
> 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.
>
> == Why now?
> It’s only recently that CSS and performance technologies/methodologies
> have matured sufficiently to define a ‘right way’ to load fonts.
>
> In particular:
> - Best practices for defining and loading fonts via CSS are now well-
> established, stable, and see broad usage elsewhere.
>
> - The increasing adoption of of HTTP/2 means that localising assets can
> (in many cases) be faster than loading them from remote sources.
>
> - 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.
>
> == The vision
> Theme developers shouldn’t have to manage their own font loading and
> optimisation. The registering and loading of fonts should be managed in
> the same way that we manage CSS and JavaScript - via abstraction, through
> an enqueue system.
>
> Whilst fonts have more moving parts than script and style files, I
> believe that `wp_enqueue_font()` could become a robust, standardised way
> of adding custom fonts to themes without radical effort or change - with
> all of the advantages we’re used to from abstracting JS/CSS (conditional
> logic, dependency management, versioning/cache-busting, consolidation of
> duplicates, etc).
>
> A 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.
>
> = The ‘Web Fundamentals’ approach
> The [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.,
>
> {{{
> @font-face {
> font-family: 'Awesome Font';
> font-style: normal;
> font-weight: 400;
> src: local('Awesome Font'),
> url('/fonts/awesome-l.woff2') format('woff2'),
> url('/fonts/awesome-l.woff') format('woff'),
> url('/fonts/awesome-l.ttf') format('truetype'),
> url('/fonts/awesome-l.eot') format('embedded-opentype');
> unicode-range: U+000-5FF; /* Latin glyphs */
> }
>
> @font-face {
> font-family: 'Awesome Font';
> font-style: normal;
> font-weight: 700;
> src: local('Awesome Font'),
> url('/fonts/awesome-l-700.woff2') format('woff2'),
> url('/fonts/awesome-l-700.woff') format('woff'),
> url('/fonts/awesome-l-700.ttf') format('truetype'),
> url('/fonts/awesome-l-700.eot') format('embedded-opentype');
> unicode-range: U+000-5FF; /* Latin glyphs */
> }
> }}}
>
> In this example, a single font (‘Awesome Font’) is being loaded, in two
> different weights (`400` and `700`). For each of those weights, multiple
> formats are provided, prioritised in such a way that considers and is
> optimized for browser support. The approach for different styles (e.g.,
> ''italic''), and all other variations, follow this pattern.
>
> In 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.
>
> 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.
>
> = A spec
> The process of enqueuing a font should allow a developer to specify the
> name of the font, and to provide an optional level of detail/control over
> specific aspects (versions, weights, file locations). Where
> details/specifics aren’t provided, sensible fallbacks and defaults should
> be assumed.
>
> I suggest the following structure:
>
> {{{
> wp_enqueue_font(
> string $handle, // The name of the font
> string|array|bool $src = false, // The source(s) of the font files
> string $style = 'normal', // The style of the font
> int $weight = 400, // The weight of the font
> string $display = 'auto' // Display/swap behaviour
> string $range = false, // A unicode range value
> string|bool $external = false, // Externalise the CSS file/code
> bool $preload = true // Should the resource be preloaded
> bool $in_footer = false // Output the CSS/file in the footer
> )
> }}}
>
> == Starting from simplicity
> A simple implementation of this, which omits much of the detail and
> utilises default behaviours, might look something like:
>
> {{{
> wp_enqueue_font(
> 'Awesome Font',
> '/fonts/awesome-font-400.woff2',
> '', // $style
> '', // $weight
> '', // $display
> '', // $range
> '', // $external
> '', // $preload
> '' // $in_footer
> )
> }}}
>
> 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:
>
> {{{
> @font-face {
> font-family: 'Awesome Font';
> font-style: normal;
> font-weight: 400;
> src: local('Awesome Font'),
> url('/fonts/awesome-font-400.woff2') format('woff2'),
> }
> }}}
>
> - ''NOTE: If no `$src` value is defined, only a local font should be
> referenced (via `$handle`).''
>
> == Supporting font registration
> 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`.
>
> == Definitions
> === $src
> Whilst 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):
>
> {{{
> wp_enqueue_font(
> 'Awesome Font',
> array(
> 'woff2' => '/fonts/awesome-font-400.woff2',
> 'woff' => '/fonts/awesome-font-400.woff',
> 'truetype' => '/fonts/awesome-font-400.ttf',
> 'embedded-opentype' => '/fonts/awesome-font-400.eot',
> ),
> '', // $style
> '', // $weight
> '', // $display
> '', // $range
> '', // $external
> '', // $preload
> '' // $in_footer
> )
> }}}
>
> - ''NOTE: If a ''type'' is invalid, the declaration should be ignored.''
>
> - ''NOTE: Data URLs (e.g., `data:application/font-
> woff2;charset=utf-8;base64[...]`) may be provided instead of file
> paths.''
>
> === $external
> When 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`).
>
> 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`).
>
> Alternatively, the value may be a string which represents a filepath,
> where the CSS ‘file’ should be accessible from (via a rewrite rule).
>
> - ''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.''
> - ''NOTE: Default location of generated CSS file TBD.''
> - ''NOTE: Invalid string values should fall back to the default
> behaviour.''
>
> === $preload
> 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).
>
> If `$external` is set to true, the associated CSS file should also be
> preloaded.
>
> - ''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].''
>
> = Other considerations
> == Unique handles
> 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.
>
> - ''NOTE: If there are non-unique instances of a font, the last-enqueued
> version should take priority.''
>
> == Invalid values & minimum requirements
> Invalid or malformed values for parameters with constrained values (e.g.,
> `$style`, `$weight`, `$display`) should fall back to their defaults.
>
> Invalid or malformed values for parameters without constrained values
> (e.g., `$range`, `$src`) should be ignored.
>
> 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.
>
> = Next steps
> 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.
>
> I’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.
New description:
= The problem
Loading 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.
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.
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).
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.
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.
== Why now?
It’s only recently that CSS and performance technologies/methodologies
have matured sufficiently to define a ‘right way’ to load fonts.
In particular:
- Best practices for defining and loading fonts via CSS are now well-
established, stable, and see broad usage elsewhere.
- The increasing adoption of of HTTP/2 means that localising assets can
(in many cases) be faster than loading them from remote sources.
- 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.
== The vision
Theme developers shouldn’t have to manage their own font loading and
optimisation. The registering and loading of fonts should be managed in
the same way that we manage CSS and JavaScript - via abstraction, through
an enqueue system.
Whilst fonts have more moving parts than script and style files, I believe
that `wp_enqueue_font()` could become a robust, standardised way of adding
custom fonts to themes without radical effort or change - with all of the
advantages we’re used to from abstracting JS/CSS (conditional logic,
dependency management, versioning/cache-busting, consolidation of
duplicates, etc).
A 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.
= The ‘Web Fundamentals’ approach
The [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.,
{{{#!css
@font-face {
font-family: 'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('/fonts/awesome-l.woff2') format('woff2'),
url('/fonts/awesome-l.woff') format('woff'),
url('/fonts/awesome-l.ttf') format('truetype'),
url('/fonts/awesome-l.eot') format('embedded-opentype');
unicode-range: U+000-5FF; /* Latin glyphs */
}
@font-face {
font-family: 'Awesome Font';
font-style: normal;
font-weight: 700;
src: local('Awesome Font'),
url('/fonts/awesome-l-700.woff2') format('woff2'),
url('/fonts/awesome-l-700.woff') format('woff'),
url('/fonts/awesome-l-700.ttf') format('truetype'),
url('/fonts/awesome-l-700.eot') format('embedded-opentype');
unicode-range: U+000-5FF; /* Latin glyphs */
}
}}}
In this example, a single font (‘Awesome Font’) is being loaded, in two
different weights (`400` and `700`). For each of those weights, multiple
formats are provided, prioritised in such a way that considers and is
optimized for browser support. The approach for different styles (e.g.,
''italic''), and all other variations, follow this pattern.
In 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.
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.
= A spec
The process of enqueuing a font should allow a developer to specify the
name of the font, and to provide an optional level of detail/control over
specific aspects (versions, weights, file locations). Where
details/specifics aren’t provided, sensible fallbacks and defaults should
be assumed.
I suggest the following structure function definition:
{{{#!php
<?php
/**
* Enqueue font.
*
* @param string $handle The name of the font.
* @param string|array|bool $src The source(s) of the font files.
* @param array $params {
* Params.
*
* @type string $style The style of the font.
* @type int $weight The weight of the font.
* @type string $display Display/swap behaviour.
* @type string $range A unicode range value.
* @type string|bool $external Externalise the CSS file/code.
* @type bool $preload Should the resource be preloaded.
* @type bool $in_footer Output the CSS/file in the footer.
* }
*/
function wp_enqueue_font( $handle, $src = false, $params = array() ) {
$params = wp_parse_args(
$params,
array(
'style' => 'normal',
'weight' => 400,
'display' => 'auto',
'range' => false,
'external' => false,
'preload' => true,
'in_footer' => false,
)
);
// ...
}}}
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()`.
== Starting from simplicity
A simple implementation of this, which omits much of the detail and
utilises default behaviours, might look something like:
{{{#!php
<?php
wp_enqueue_font(
'Awesome Font',
'/fonts/awesome-font-400.woff2'
);
}}}
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:
{{{#!css
@font-face {
font-family: 'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('/fonts/awesome-font-400.woff2') format('woff2'),
}
}}}
- ''NOTE: If no `$src` value is defined, only a local font should be
referenced (via `$handle`).''
== Supporting font registration
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`.
== Definitions
=== $src
Whilst 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):
{{{#!php
<?php
wp_enqueue_font(
'Awesome Font',
array(
'woff2' => '/fonts/awesome-font-400.woff2',
'woff' => '/fonts/awesome-font-400.woff',
'truetype' => '/fonts/awesome-font-400.ttf',
'embedded-opentype' => '/fonts/awesome-font-400.eot',
)
);
}}}
- ''NOTE: If a ''type'' is invalid, the declaration should be ignored.''
- ''NOTE: Data URLs (e.g., `data:application/font-
woff2;charset=utf-8;base64[...]`) may be provided instead of file paths.''
=== $external
When 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`).
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`).
Alternatively, the value may be a string which represents a filepath,
where the CSS ‘file’ should be accessible from (via a rewrite rule).
- ''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.''
- ''NOTE: Default location of generated CSS file TBD.''
- ''NOTE: Invalid string values should fall back to the default
behaviour.''
=== $preload
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).
If `$external` is set to true, the associated CSS file should also be
preloaded.
- ''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].''
= Other considerations
== Unique handles
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.
- ''NOTE: If there are non-unique instances of a font, the last-enqueued
version should take priority.''
== Invalid values & minimum requirements
Invalid or malformed values for parameters with constrained values (e.g.,
`$style`, `$weight`, `$display`) should fall back to their defaults.
Invalid or malformed values for parameters without constrained values
(e.g., `$range`, `$src`) should be ignored.
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.
= Next steps
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.
I’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.
--
--
Ticket URL: <https://core.trac.wordpress.org/ticket/46370#comment:7>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list