PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.2
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.2
2.0.4 2.0.3 2.0.2 2.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2 1.3 2.0.0
easyfonts / assets / frontend / beacon.js

beacon.js in EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant 2.0.2, at assets/frontend/beacon.js

164 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Easy Fonts beacon.
3 *
4 * Measures real font usage in the browser and reports decisions to the server:
5 * - which font variants render ABOVE THE FOLD -> preload candidates
6 * - which loaded faces never render anywhere -> unload candidates
7 *
8 * Sources: document.fonts (FontFaceSet) for what loaded, getComputedStyle for
9 * what's actually applied, getBoundingClientRect for above-the-fold.
10 */
11 ( function () {
12 'use strict';
13
14 var CFG = window.EasyFontsBeacon;
15 if ( ! CFG || ! CFG.endpoint || ! document.fonts ) {
16 return;
17 }
18
19 var GENERIC = /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-(?:serif|sans-serif|monospace|rounded)|-apple-system|blinkmacsystemfont|"?segoe ui"?|helvetica|helvetica neue|arial|times new roman|georgia|courier new|inherit|initial|revert|unset|emoji|math|fangsong)$/i;
20
21 function clean( name ) {
22 return ( name || '' ).trim().replace( /^["']|["']$/g, '' );
23 }
24
25 function normWeight( w ) {
26 if ( w === 'normal' ) { return '400'; }
27 if ( w === 'bold' ) { return '700'; }
28 return String( w || '400' );
29 }
30
31 function collect() {
32 var loaded = {}; // family -> { weight: {style:true} } (what the browser fetched)
33 var rendered = {}; // family|weight|style -> true
34 var aboveFold = {};// family|weight|style -> true
35 var foldLine = window.innerHeight || document.documentElement.clientHeight;
36
37 // 1. Loaded faces.
38 document.fonts.forEach( function ( face ) {
39 if ( face.status !== 'loaded' ) { return; }
40 var fam = clean( face.family );
41 if ( ! fam || GENERIC.test( fam ) ) { return; }
42 var w = normWeight( face.weight );
43 var s = face.style || 'normal';
44 loaded[ fam ] = loaded[ fam ] || {};
45 loaded[ fam ][ w + '|' + s ] = true;
46 } );
47
48 // 2. Rendered + above-the-fold (sample visible text elements). The set
49 // of element types is deliberately broad — a family is only kept on a
50 // page if something here reports it, so missing an element type would
51 // wrongly scope a used font off the page.
52 var nodes = document.querySelectorAll(
53 'h1,h2,h3,h4,h5,h6,p,a,span,li,td,th,caption,button,label,input,textarea,select,option,' +
54 'blockquote,figcaption,figure,dl,dt,dd,summary,details,strong,b,em,i,u,mark,small,' +
55 'cite,q,code,pre,abbr,time,address,legend,output,div'
56 );
57 var max = Math.min( nodes.length, 3000 ); // cap work on huge pages
58
59 function noteRendered( fam, weight, style, rect ) {
60 if ( ! fam || GENERIC.test( fam ) ) { return; }
61 var key = fam + '|' + normWeight( weight ) + '|' + ( style || 'normal' );
62 rendered[ key ] = true;
63 if ( rect && rect.top < foldLine && rect.bottom > 0 && rect.width > 0 && rect.height > 0 ) {
64 aboveFold[ key ] = true;
65 }
66 }
67
68 for ( var i = 0; i < max; i++ ) {
69 var el = nodes[ i ];
70
71 var hasText = el.value ? true : ( el.textContent && el.textContent.trim() );
72 if ( ! hasText ) { continue; }
73
74 var rect = el.getBoundingClientRect();
75 var cs = getComputedStyle( el );
76 noteRendered( clean( ( cs.fontFamily || '' ).split( ',' )[ 0 ] ), cs.fontWeight, cs.fontStyle, rect );
77
78 // Generated content (::before / ::after) can carry its own font.
79 var bef = getComputedStyle( el, '::before' );
80 if ( bef && bef.content && bef.content !== 'none' && bef.content !== 'normal' ) {
81 noteRendered( clean( ( bef.fontFamily || '' ).split( ',' )[ 0 ] ), bef.fontWeight, bef.fontStyle, rect );
82 }
83 var aft = getComputedStyle( el, '::after' );
84 if ( aft && aft.content && aft.content !== 'none' && aft.content !== 'normal' ) {
85 noteRendered( clean( ( aft.fontFamily || '' ).split( ',' )[ 0 ] ), aft.fontWeight, aft.fontStyle, rect );
86 }
87 }
88
89 // 3. Build decisions.
90 var preload = [];
91 Object.keys( aboveFold ).forEach( function ( key ) {
92 var p = key.split( '|' );
93 preload.push( { family: p[ 0 ], weight: p[ 1 ], style: p[ 2 ] } );
94 } );
95
96 var unload = [];
97 Object.keys( loaded ).forEach( function ( fam ) {
98 Object.keys( loaded[ fam ] ).forEach( function ( ws ) {
99 var parts = ws.split( '|' );
100 var key = fam + '|' + parts[ 0 ] + '|' + parts[ 1 ];
101 if ( ! rendered[ key ] ) {
102 unload.push( { family: fam, weight: parts[ 0 ], style: parts[ 1 ] } );
103 }
104 } );
105 } );
106
107 var renderedList = Object.keys( rendered ).map( function ( key ) {
108 var p = key.split( '|' );
109 return {
110 family: p[ 0 ],
111 weight: p[ 1 ],
112 style: p[ 2 ],
113 above_fold: aboveFold[ key ] ? 1 : 0
114 };
115 } );
116
117 return { rendered: renderedList, preload: preload, unload: unload };
118 }
119
120 function send( data ) {
121 if ( ! data.rendered.length && ! data.preload.length ) { return; }
122
123 var payload = JSON.stringify( {
124 route: CFG.route,
125 device: CFG.device,
126 rendered: data.rendered,
127 preload: data.preload,
128 unload: data.unload
129 } );
130
131 var url = CFG.endpoint + '?_wpnonce=' + encodeURIComponent( CFG.nonce );
132
133 if ( navigator.sendBeacon ) {
134 navigator.sendBeacon( url, new Blob( [ payload ], { type: 'application/json' } ) );
135 } else {
136 var xhr = new XMLHttpRequest();
137 xhr.open( 'POST', url, true );
138 xhr.setRequestHeader( 'Content-Type', 'application/json' );
139 xhr.setRequestHeader( 'X-WP-Nonce', CFG.nonce );
140 xhr.send( payload );
141 }
142 }
143
144 // Run after fonts settle, giving lazy/builder fonts time to apply. We also
145 // flush on pagehide/visibilitychange so the report is never lost — important
146 // inside the admin's hidden optimize iframe, which may be torn down quickly.
147 var sent = false;
148
149 function flush() {
150 if ( sent ) { return; }
151 sent = true;
152 try { send( collect() ); } catch ( e ) {}
153 }
154
155 document.fonts.ready.then( function () {
156 setTimeout( flush, 1500 );
157 } );
158
159 window.addEventListener( 'pagehide', flush );
160 document.addEventListener( 'visibilitychange', function () {
161 if ( document.visibilityState === 'hidden' ) { flush(); }
162 } );
163 }() );
164