PluginProbe
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant / 2.0.1
EasyFonts – Host Google Fonts Locally, Fast & Auto-Optimize, GDPR Compliant v2.0.1
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.1, at assets/frontend/beacon.js

146 lines 4.6 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).
49 var nodes = document.querySelectorAll(
50 'h1,h2,h3,h4,h5,h6,p,a,span,li,td,th,button,label,blockquote,figcaption,strong,em,div'
51 );
52 var max = Math.min( nodes.length, 2500 ); // cap work on huge pages
53
54 for ( var i = 0; i < max; i++ ) {
55 var el = nodes[ i ];
56 if ( ! el.textContent || ! el.textContent.trim() ) { continue; }
57
58 var cs = getComputedStyle( el );
59 var first = clean( ( cs.fontFamily || '' ).split( ',' )[ 0 ] );
60 if ( ! first || GENERIC.test( first ) ) { continue; }
61
62 var key = first + '|' + normWeight( cs.fontWeight ) + '|' + ( cs.fontStyle || 'normal' );
63 rendered[ key ] = true;
64
65 var rect = el.getBoundingClientRect();
66 if ( rect.top < foldLine && rect.bottom > 0 && rect.width > 0 && rect.height > 0 ) {
67 aboveFold[ key ] = true;
68 }
69 }
70
71 // 3. Build decisions.
72 var preload = [];
73 Object.keys( aboveFold ).forEach( function ( key ) {
74 var p = key.split( '|' );
75 preload.push( { family: p[ 0 ], weight: p[ 1 ], style: p[ 2 ] } );
76 } );
77
78 var unload = [];
79 Object.keys( loaded ).forEach( function ( fam ) {
80 Object.keys( loaded[ fam ] ).forEach( function ( ws ) {
81 var parts = ws.split( '|' );
82 var key = fam + '|' + parts[ 0 ] + '|' + parts[ 1 ];
83 if ( ! rendered[ key ] ) {
84 unload.push( { family: fam, weight: parts[ 0 ], style: parts[ 1 ] } );
85 }
86 } );
87 } );
88
89 var renderedList = Object.keys( rendered ).map( function ( key ) {
90 var p = key.split( '|' );
91 return {
92 family: p[ 0 ],
93 weight: p[ 1 ],
94 style: p[ 2 ],
95 above_fold: aboveFold[ key ] ? 1 : 0
96 };
97 } );
98
99 return { rendered: renderedList, preload: preload, unload: unload };
100 }
101
102 function send( data ) {
103 if ( ! data.rendered.length && ! data.preload.length ) { return; }
104
105 var payload = JSON.stringify( {
106 route: CFG.route,
107 device: CFG.device,
108 rendered: data.rendered,
109 preload: data.preload,
110 unload: data.unload
111 } );
112
113 var url = CFG.endpoint + '?_wpnonce=' + encodeURIComponent( CFG.nonce );
114
115 if ( navigator.sendBeacon ) {
116 navigator.sendBeacon( url, new Blob( [ payload ], { type: 'application/json' } ) );
117 } else {
118 var xhr = new XMLHttpRequest();
119 xhr.open( 'POST', url, true );
120 xhr.setRequestHeader( 'Content-Type', 'application/json' );
121 xhr.setRequestHeader( 'X-WP-Nonce', CFG.nonce );
122 xhr.send( payload );
123 }
124 }
125
126 // Run after fonts settle, giving lazy/builder fonts time to apply. We also
127 // flush on pagehide/visibilitychange so the report is never lost — important
128 // inside the admin's hidden optimize iframe, which may be torn down quickly.
129 var sent = false;
130
131 function flush() {
132 if ( sent ) { return; }
133 sent = true;
134 try { send( collect() ); } catch ( e ) {}
135 }
136
137 document.fonts.ready.then( function () {
138 setTimeout( flush, 1500 );
139 } );
140
141 window.addEventListener( 'pagehide', flush );
142 document.addEventListener( 'visibilitychange', function () {
143 if ( document.visibilityState === 'hidden' ) { flush(); }
144 } );
145 }() );
146