<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Performance | Julien Maxant</title><link>https://www.julien-maxant.com/tags/performance/</link><description>Articles, projets et veille sur le thème « Performance ».</description><generator>Hugo -- gohugo.io</generator><language>fr-FR</language><copyright>2026 - Julien Maxant</copyright><lastBuildDate>Wed, 23 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.julien-maxant.com/tags/performance/index.xml" rel="self" type="application/rss+xml"/><item><title>Compiler sa propre police variable : le cas Spectral</title><link>https://www.julien-maxant.com/blog/construire-une-police-variable/</link><guid>https://www.julien-maxant.com/blog/construire-une-police-variable/</guid><pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate><description>Pas de version variable officielle pour la police serif de ce site : reconstruire un font variable à partir des sources, quatre fois plus léger que les fichiers statiques.</description><content:encoded><![CDATA[<p>Ce site sert la police serif Spectral en 2 fichiers <code>.woff2</code> de ~64 Ko chacun. Avant,
c&rsquo;était 6 fichiers statiques pour ~532 Ko au total — un par graisse (light, regular,
semibold) et par style (romain, italique). Le problème : Spectral n&rsquo;a <strong>aucune version
variable officielle</strong>. Google Fonts ne la sert qu&rsquo;en statique. Il a donc fallu la
compiler soi-même à partir des sources.</p>
<h2 id="statique-vs-variable-en-une-phrase">Statique vs variable, en une phrase</h2>
<p>Une police &ldquo;statique&rdquo; est un fichier par graisse : un pour le 300, un pour le 400, un
pour le 600. Une police &ldquo;variable&rdquo; encode un axe continu (<code>wght</code>) dans un seul
fichier — le navigateur interpole entre les graisses définies plutôt que de charger
un fichier différent pour chacune. Sur ce site, <code>--font-weight-light</code> (300),
<code>--font-weight-normal</code> (400) et <code>--font-weight-bold</code> (600) tiennent donc dans un seul
fichier par style, romain et italique.</p>
<h2 id="pas-de-release-officielle-mais-des-sources-buildables">Pas de release officielle, mais des sources buildables</h2>
<p>Les sources UFO (le format d&rsquo;échange des designers de fontes) de Spectral sont
publiques, maintenues par Google, dans
<a href="https://github.com/googlefonts/spectral"><code>googlefonts/spectral</code></a> (licence OFL-1.1).
Elles incluent deux fichiers <code>.designspace</code> — un pour le romain, un pour l&rsquo;italique —
qui décrivent comment interpoler entre les masters de graisse. Le pipeline de build
officiel du dépôt ne produit que la release statique et dépend d&rsquo;une demi-douzaine
d&rsquo;outils externes (<code>afdko</code>, <code>ttfautohint</code>&hellip;). Recompiler la même source avec
<code>fontmake -o variable</code> produit une police variable, avec seulement <code>fontmake</code> et
<code>fonttools</code> — les deux seuls outils dont ce projet a besoin (<code>docs/fonts.md</code>).</p>
<p>Comme Debian empêche un <code>pip install</code> au niveau système et que le module <code>venv</code> n&rsquo;est
pas toujours présent sans <code>sudo</code>, la toolchain Python vit dans un venv local créé par
<a href="https://docs.astral.sh/uv/"><code>uv</code></a> (<code>task setup:fonts</code>), avec les versions figées dans
<code>scripts/fonts/requirements.txt</code> — le même principe que <code>package.json</code> pour les
dépendances JS.</p>
<h2 id="ne-garder-que-ce-qui-sert">Ne garder que ce qui sert</h2>
<p>Rien n&rsquo;oblige à livrer la police telle que ses sources la définissent :</p>
<ul>
<li><strong>L&rsquo;axe de graisse</strong> couvre 200 à 800 en amont ; seuls 300/400/600 sont utilisés
dans ce projet. Après le premier build, <code>fonttools varLib.instancer</code> restreint
l&rsquo;axe à 300–600 — la police reste variable, juste sur une plage plus étroite.</li>
<li><strong>Le jeu de glyphes</strong> couvre en amont le cyrillique, le grec, l&rsquo;arménien, le
géorgien — 1481 glyphes, aucun atteignable par du contenu français (ou anglais).
<code>fonttools subset</code> réduit à Latin + Latin-1 + Latin étendu A/B + ponctuation
générale + <code>€</code>, soit 810 glyphes — de quoi couvrir le français, l&rsquo;anglais, et une
marge pour la plupart des langues latines (polonais, tchèque, turc&hellip;).</li>
</ul>
<p>Combiné, l&rsquo;axe restreint et le sous-ensemble de glyphes font passer romain + italique
de ~532 Ko à ~128 Ko : environ 4 fois plus léger, pour un rendu strictement identique
sur tout ce que le site affiche réellement.</p>
<h2 id="trois-bugs-de-compilation-qui-ne-se-voient-quà-lusage">Trois bugs de compilation qui ne se voient qu&rsquo;à l&rsquo;usage</h2>
<p>Les sources brutes ne compilent pas telles quelles :</p>
<ol>
<li><strong>L&rsquo;axe <code>wght</code> n&rsquo;a pas de correspondance déclarée.</strong> En interne, les coordonnées de
design de Spectral ne sont pas linéaires — le SemiBold est stocké à la position 440
plutôt qu&rsquo;à 600. Sans <code>&lt;map&gt;</code> pour traduire les valeurs standard OpenType
(200/300&hellip;/800) vers ces coordonnées internes, une règle CSS comme
<code>font-weight: 300 600</code> aurait fini par pointer vers la mauvaise zone de l&rsquo;espace de
design. Un <code>&lt;map&gt;</code> a été ajouté aux deux <code>.designspace</code> avant le build.</li>
<li><strong>Des glyphes structurellement incompatibles entre masters</strong> (quelques glyphes
cyrilliques, des compositions d&rsquo;accents) — jamais un problème pour le pipeline
statique de Spectral, qui n&rsquo;a jamais besoin d&rsquo;interpoler entre eux. Plutôt que de
les supprimer (ce qui aurait cassé des références dans <code>features.fea</code>), leurs
contours ont été rendus identiques entre masters en copiant ceux du master
Regular — sans effet sur le contenu réel du site.</li>
<li><strong>Une entrée cmap ambiguë</strong> : <code>apostrophemod</code> et <code>uni02BC</code> réclamaient le même
point de code (U+02BC), ce que <code>ufo2ft</code> refuse. Le point de code a été retiré de
l&rsquo;un des deux.</li>
</ol>
<p>Vérification avant commit — que l&rsquo;axe <code>fvar</code> reflète bien 300/400/600 et pas les
coordonnées internes brutes :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-py" data-lang="py"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fontTools.ttLib</span> <span class="kn">import</span> <span class="n">TTFont</span>
</span></span><span class="line"><span class="cl"><span class="n">f</span> <span class="o">=</span> <span class="n">TTFont</span><span class="p">(</span><span class="s1">&#39;assets/styles/fonts/Spectral-Variable.woff2&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">a</span> <span class="o">=</span> <span class="n">f</span><span class="p">[</span><span class="s1">&#39;fvar&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="n">axisTag</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">minValue</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">defaultValue</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">maxValue</span><span class="p">)</span></span></span></code></pre></div><h2 id="deux-fichiers-pas-un">Deux fichiers, pas un</h2>
<p>Une police variable peut en théorie combiner plusieurs axes — graisse <em>et</em> italique
dans un seul fichier, avec un axe <code>ital</code>. Les sources de Spectral n&rsquo;en ont pas :
chaque <code>.designspace</code> ne porte qu&rsquo;un axe <code>wght</code>. Fusionner romain et italique
demanderait de retravailler les sources UFO elles-mêmes pour leur ajouter un axe —
un travail de conception de police, pas de compilation, et fragile à maintenir face
aux mises à jour amont. Deux fichiers restent le plancher réaliste ici, contre six
avant.</p>
<h2 id="licence--pas-de-renommage-nécessaire">Licence : pas de renommage nécessaire</h2>
<p>Le nom d&rsquo;une police modifiée ne doit être changé, sous OFL, que si la licence
originale déclare un <em>Reserved Font Name</em> — pas systématiquement à chaque
modification. <code>assets/styles/fonts/OFL.txt</code> n&rsquo;en déclare aucun : rien ne suit la
ligne de copyright, et &ldquo;Spectral&rdquo; n&rsquo;apparaît nulle part ailleurs dans le texte de
licence. Cette compilation reste une &ldquo;Modified Version&rdquo; au sens de l&rsquo;OFL (reconstruite
depuis les sources, axe remappé, glyphes normalisés et sous-ensemble), mais conserve
légitimement le nom &ldquo;Spectral&rdquo; — dans les métadonnées internes de la police comme
dans <code>--font-serif</code>.</p>
]]></content:encoded></item></channel></rss>