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 / async-blocker.js

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

157 lines 4.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Easy Fonts — async Google Fonts blocker.
3 *
4 * Some themes/plugins inject Google/Bunny Fonts at RUNTIME via JavaScript
5 * (document.createElement('link') + appendChild, or an @import in an injected
6 * <style>). Those never appear in the server-rendered HTML, so the PHP output
7 * buffer can't see them. This script:
8 *
9 * 1. Intercepts provider stylesheet <link>s before they enter the DOM and
10 * neutralises them (so the render-blocking Google request is prevented),
11 * 2. Reports the URLs to the server, which self-hosts them on the next render.
12 *
13 * After the first visit the fonts are hosted locally, so the consolidated local
14 * stylesheet covers them and the remote request is gone for everyone.
15 */
16 ( function () {
17 'use strict';
18
19 var CFG = window.EasyFontsAsync;
20 if ( ! CFG || ! CFG.endpoint ) {
21 return;
22 }
23
24 // Google Fonts (and the WordPress.com Google Fonts proxy) only — Bunny is
25 // intentionally excluded from async blocking.
26 var HOSTS = [ 'fonts.googleapis.com', 'fonts-api.wp.com' ];
27 var found = {};
28
29 function isProviderCss( url ) {
30 if ( ! url || typeof url !== 'string' ) {
31 return false;
32 }
33 for ( var i = 0; i < HOSTS.length; i++ ) {
34 if ( url.indexOf( HOSTS[ i ] + '/css' ) !== -1 || url.indexOf( HOSTS[ i ] + '/icon' ) !== -1 ) {
35 return true;
36 }
37 }
38 return false;
39 }
40
41 function record( url ) {
42 if ( ! url || found[ url ] ) {
43 return;
44 }
45 found[ url ] = true;
46 schedule();
47 }
48
49 // Neutralise a provider <link> so the browser never fetches it.
50 function neutralise( node ) {
51 try {
52 var href = node.getAttribute ? node.getAttribute( 'href' ) : node.href;
53 if ( node.tagName === 'LINK' && isProviderCss( href ) ) {
54 record( href );
55 // Prevent the fetch: blank rel/href + mark inert.
56 node.setAttribute( 'data-easyfonts-async', '1' );
57 node.setAttribute( 'media', 'max-width:1px' );
58 node.setAttribute( 'href', 'data:text/css,' );
59 node.rel = 'preload';
60 return true;
61 }
62 } catch ( e ) {}
63 return false;
64 }
65
66 // 1. Intercept programmatic insertion (catches it before the request fires).
67 function patch( proto, method ) {
68 if ( ! proto || ! proto[ method ] || proto[ method ].__efPatched ) {
69 return;
70 }
71 var original = proto[ method ];
72 proto[ method ] = function ( node ) {
73 try {
74 if ( node && node.tagName === 'LINK' ) {
75 neutralise( node );
76 }
77 } catch ( e ) {}
78 return original.apply( this, arguments );
79 };
80 proto[ method ].__efPatched = true;
81 }
82
83 patch( Node.prototype, 'appendChild' );
84 patch( Node.prototype, 'insertBefore' );
85
86 // 2. Safety net: observe anything that still slips into the DOM.
87 function sweep() {
88 var links = document.querySelectorAll( 'link[rel="stylesheet"],link[as="style"]' );
89 for ( var i = 0; i < links.length; i++ ) {
90 if ( ! links[ i ].getAttribute( 'data-easyfonts-async' ) ) {
91 neutralise( links[ i ] );
92 }
93 }
94 }
95
96 if ( window.MutationObserver ) {
97 var obs = new MutationObserver( function ( mutations ) {
98 for ( var i = 0; i < mutations.length; i++ ) {
99 var added = mutations[ i ].addedNodes;
100 for ( var j = 0; j < added.length; j++ ) {
101 if ( added[ j ].tagName === 'LINK' ) {
102 neutralise( added[ j ] );
103 }
104 }
105 }
106 } );
107 try {
108 obs.observe( document.documentElement, { childList: true, subtree: true } );
109 } catch ( e ) {}
110 }
111
112 // Report collected URLs (debounced, once settled).
113 var timer = null;
114 function schedule() {
115 if ( timer ) {
116 clearTimeout( timer );
117 }
118 timer = setTimeout( send, 800 );
119 }
120
121 var sent = {};
122 function send() {
123 var urls = [];
124 Object.keys( found ).forEach( function ( u ) {
125 if ( ! sent[ u ] ) {
126 urls.push( u );
127 sent[ u ] = true;
128 }
129 } );
130 if ( ! urls.length ) {
131 return;
132 }
133
134 var payload = JSON.stringify( { urls: urls } );
135 var url = CFG.endpoint + '?_wpnonce=' + encodeURIComponent( CFG.nonce );
136
137 if ( navigator.sendBeacon ) {
138 navigator.sendBeacon( url, new Blob( [ payload ], { type: 'application/json' } ) );
139 } else {
140 try {
141 var xhr = new XMLHttpRequest();
142 xhr.open( 'POST', url, true );
143 xhr.setRequestHeader( 'Content-Type', 'application/json' );
144 xhr.setRequestHeader( 'X-WP-Nonce', CFG.nonce );
145 xhr.send( payload );
146 } catch ( e ) {}
147 }
148 }
149
150 if ( document.readyState !== 'loading' ) {
151 sweep();
152 } else {
153 document.addEventListener( 'DOMContentLoaded', sweep );
154 }
155 window.addEventListener( 'load', function () { sweep(); schedule(); } );
156 }() );
157