Aproveitando a API do Google Fonts para Otimizar o Carregamento de Fontes
Como já havia comentado nas Dicas para um site nota 100.
O Google Fonts é amplamente conhecido e usado em muitos sistemas, mas você sabia que ele possui uma API aberta? Com a API do Google Fonts, é possível carregar apenas os estilos e caracteres específicos necessários para o seu site, como fontes em itálico, o que pode otimizar o carregamento.
No exemplo a seguir, você pode definir apenas os caracteres em itálico usados no seu site:
const base = "https://fonts.googleapis.com/css2"
const chars = 'Olá mundo';
const text = encodeURIComponent();
const italic = true;
const fontWeight = "400";
const family = `Roboto:${italic ? ("ital,wght@1," + fontWeight ) : ("wght@" + fontWeight)}`
const queryArgs = {
display: "swap",
text,
};
const url = base + "?family=" + family + "&" + new URLSearchParams(queryArgs).toString();
//https://fonts.googleapis.com/css2?family=Roboto:ital,wght@1,400&display=swap&text=Ol%25C3%25A1mundo
Esse código gera o link para importar o font-face
desejado:
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/l/font?kit={URL_PARA_ARQUIVO_DA_FONTE}) format('{FORMATO_DO_ARQUIVO}');
}
Para melhorar o carregamento da página, você pode adicionar o preconnect ao CDN do Google Fonts (https://fonts.gstatic.com) e usar preload para carregar o arquivo da fonte:
<link href="https://fonts.gstatic.com/l/font?kit={URL_PARA_ARQUIVO_DA_FONTE}" rel="preload" crossorigin="anonymous" as="font" type="font/{FORMATO_DO_ARQUIVO}" fetchpriority="high">
Extra : Como extrair as fontes de uma página estática a partir do style tag :
Abaixo uma pequena implementação simplificada (porém funcional) para extrair os caracteres e suas fontes do html estático utlizando a biblioteca Cheerio.
GoogleFontsClient.js
Este é o cliente para solicitar o font-face
à API do Google Fonts e extrair o link do arquivo de fonte para realizar o preload
e o CSS que será adicionado ao cabeçalho da página:
class GoogleFontsClient {
static generateFontUrl(charset, fontFamily) {
const fontUrls = [];
Object.keys(charset).forEach((fontWeight) => {
const text = charset[fontWeight];
const baseChars = '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split("");
text.forEach((char) => {
if (!baseChars.includes(char)) {
baseChars.push(char);
}
});
let charSet = baseChars.join("");
let chars = '';
try {
chars = encodeURIComponent(charSet.trim().replace(/[^\p{L}\p{N}\p{P}\p{Z}^$~0123456789]/gu, '').replace(/\s/g, ''));
} catch (e) {
chars = null;
}
if (chars.length > 0) {
if (fontWeight.indexOf(':ital') !== -1) {
fontWeight = fontWeight.replace(":ital", "");
fontUrls.push('https://fonts.googleapis.com/css2?family=' + fontFamily + ':ital,wght@1,' + fontWeight + '&display=swap&text=' + chars);
} else {
fontUrls.push('https://fonts.googleapis.com/css2?family=' + fontFamily + ':wght@' + fontWeight + '&display=swap&text=' + chars);
}
}
});
return fontUrls;
}
static async getMinifiedFont(charset, fontFamily) {
if (!fontFamily) {
return [];
}
fontFamily = fontFamily.replaceAll('"', '').replaceAll(" ", "+");
if (charset.length === 0) {
return [];
}
const fontUrls = GoogleFontsClient.generateFontUrl(charset, fontFamily);
const fontFaces = [];
for (const url of fontUrls) {
const { css, preload } = await GoogleFontsClient.downloadFont(url);
if (!css) continue;
fontFaces.push({ url, css, preload });
}
return fontFaces;
}
static async downloadFont(url) {
const response = await fetch(url);
if (!response.ok || response.status !== 200) {
return {};
}
const text = await response.text();
let regex = /https:\/\/fonts\.gstatic\.com\/[^\s)]+/g;
let matches = text.match(regex);
if (!matches) {
return {
css: text,
preload: new Set()
};
}
const uniqueMatches = Array.from(new Set(matches));
return {
css: text,
preload: uniqueMatches
};
}
}
module.exports = { GoogleFontsClient };
FontExtractor.js
Esse arquivo extrai os caracteres a partir do atributo style
de uma tag no HTML, verificando os elementos para identificar caracteres em itálico ou negrito:
const cheerio = require('cheerio');
class FontExtractor {
static async extract(htmlContent, { defaultFontFamily = 'Roboto' }) {
const $ = cheerio.load(htmlContent);
const fontStyles = {};
$('*').each((i, tag) => {
if (tag.attribs.style && tag.attribs.style.indexOf('font-family') !== -1) {
const styleRules = FontExtractor.parseRules(tag.attribs.style.trim());
if (styleRules) {
FontExtractor.fetchFonts(fontStyles, styleRules, $, tag);
}
}
});
return FontExtractor.formatFontStyles(fontStyles);
}
static parseRules(rules) {
return rules.split(';').reduce((obj, rule) => {
const [key, value] = rule.split(': ').map(rule => rule.trim());
obj[key] = value;
return obj;
}, {});
}
static fetchFonts(fontStyles, styleRules, $, tag, text = '') {
const recursiveHandler = (child) => {
const children = $(child).children().toArray();
if (children.length > 0) {
children.forEach(rChild => recursiveHandler(rChild));
}
let fontFamily = styleRules['font-family'];
let italicChars = '';
let boldChars = '';
if (child.name === 'em' || $(child).attr('style')?.includes('italic')) {
italicChars = $(child).text() + text.replace(/\s/g, '');
}
if (child.name === 'strong' || $(child).attr('style')?.includes('bold')) {
boldChars = $(child).text() + text.replace(/\s/g, '');
}
const isNormal = !(italicChars || boldChars);
if (italicChars) {
FontExtractor.getFonts(fontStyles, { 'font-family': fontFamily }, italicChars, 'italic');
}
if (boldChars) {
FontExtractor.getFonts(fontStyles, { 'font-family': fontFamily }, boldChars, 'bold');
}
if (isNormal) {
FontExtractor.getFonts(fontStyles, { 'font-family': fontFamily }, $(child).text() + text.replace(/\s/g, ''), 'normal');
}
};
recursiveHandler(tag);
}
static getFonts(fontStyles, styleRules, text, type = 'normal') {
const fontFamily = styleRules['font-family'];
const textChars = new Set(text.trim().replace(/\s/g, '').split(""));
if (!fontStyles[fontFamily]) {
fontStyles[fontFamily] = {
normal: new Set(),
bold: new Set(),
italic: new Set(),
'bold-italic': new Set(),
};
}
for (const char of textChars) {
fontStyles[fontFamily][type].add(char);
}
}
static formatFontStyles(fontStyles) {
return Object.keys(fontStyles).map(fontFamily => ({
charset: {
'400': Array.from(fontStyles[fontFamily].normal),
'700': Array.from(fontStyles[fontFamily].bold),
'400:ital': Array.from(fontStyles[fontFamily].italic),
'700:ital': Array.from(fontStyles[fontFamily]['bold-italic']),
},
source: 'google-fonts',
fontFamily,
}));
}
}
module.exports = { FontExtractor };
index.js
const { GoogleFontsClient } = require('./GoogleFontsClient');
const { FontExtractor } = require('./FontExtractor');
const fonts = FontExtractor.extract(htmlString);
for (const font of fonts) {
const {charset, fontFamily} = font;
const data = await GoogleFontsClient.getMinifiedFont(charset, fontFamily);
result.push(data);
};
package.json
{
"name": "google-fonts-extractor",
"version": "1.0.0",
"description": "",
"main": "index.js",
"author": "[email protected]",
"dependencies": {
"cheerio": "^1.0.0-rc.12",
}
}