<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tomáš Janoušek</title>
    <description>The latest articles on Forem by Tomáš Janoušek (@liskin).</description>
    <link>https://forem.com/liskin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F509632%2F7a5188eb-5f8e-49d9-b315-dfd27ca6dc4b.png</url>
      <title>Forem: Tomáš Janoušek</title>
      <link>https://forem.com/liskin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/liskin"/>
    <language>en</language>
    <item>
      <title>Even faster bash startup</title>
      <dc:creator>Tomáš Janoušek</dc:creator>
      <pubDate>Wed, 25 Nov 2020 00:00:00 +0000</pubDate>
      <link>https://forem.com/liskin/even-faster-bash-startup-3943</link>
      <guid>https://forem.com/liskin/even-faster-bash-startup-3943</guid>
      <description>&lt;p&gt;I sped up bash startup from 165 ms to 40 ms. It’s actually noticeable. Why and how did I do it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Table of Contents
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Motivation&lt;/li&gt;
&lt;li&gt;
Investigation

&lt;ul&gt;
&lt;li&gt;man&lt;/li&gt;
&lt;li&gt;death by a thousand cuts&lt;/li&gt;
&lt;li&gt;completions&lt;/li&gt;
&lt;li&gt;fzf&lt;/li&gt;
&lt;li&gt;are we done yet?&lt;/li&gt;
&lt;li&gt;history&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;Up­date 1: Why not fix typ­ing be­fore the prompt in­stead?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Motivation
&lt;/h3&gt;

&lt;p&gt;Whenever I need to quickly look something up (or use a calculator), I open a new terminal (using a keyboard shortcut) and start typing into it. Slow bash startup disrupts this workflow as I would often type before the shell prompt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IWNarVOZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/mistype.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IWNarVOZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/mistype.png" alt="messed up prompt" width="205" height="51"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/danpker"&gt;Daniel Parker&lt;/a&gt; recently wrote an excellent blog post &lt;a href="https://danpker.com/posts/2020/faster-bash-startup/"&gt;Faster Bash Startup&lt;/a&gt; detailing his journey from 1.7 seconds to 210 ms. I start at 165 ms and need to go significantly lower than Daniel, therefore different techniques will be needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Investigation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/sharkdp/hyperfine"&gt;hyperfine&lt;/a&gt; is a brilliant command-line tool for benchmarking commands that I discovered recently (thanks to Daniel!), so let’s see where we are now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tomi@notes ~]$ hyperfine 'bash -i'
Benchmark #1: bash -i
  Time (mean ± σ): 165.8 ms ± 0.7 ms [User: 156.3 ms, System: 12.8 ms]
  Range (min … max): 164.9 ms … 167.1 ms 17 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to find out what’s taking so long. &lt;a href="https://stackoverflow.com/questions/5014823/how-to-profile-a-bash-shell-script-slow-startup/20855353"&gt;How to profile a bash shell script slow startup?&lt;/a&gt; Most Stack Overflow answers suggest some variant of &lt;code&gt;set -x&lt;/code&gt;, which will help us find any single command that takes unusually long.&lt;/p&gt;

&lt;h4&gt;
  
  
  man
&lt;/h4&gt;

&lt;p&gt;In my case, that command was &lt;code&gt;man -w&lt;/code&gt;, specifically &lt;a href="https://github.com/liskin/dotfiles/blob/7d14190467fe22bf5d4f85a7b202118d2341e3ed/.bashrc.d/10_env.sh#L8-L10"&gt;this piece of my &lt;code&gt;.bashrc.d/10_env.sh&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MANPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.local/share/man:
&lt;span class="c"&gt;# FIXME: workaround for /usr/share/bash-completion/completions/man&lt;/span&gt;
&lt;span class="nv"&gt;MANPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;man &lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turns out none of this is needed any more, &lt;code&gt;man&lt;/code&gt; and &lt;code&gt;manpath&lt;/code&gt; now add &lt;code&gt;~/.local/share/man&lt;/code&gt; automatically so I can just drop it and save more than 100 ms&lt;sup id="fnref:man-seccomp"&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  death by a thousand cuts
&lt;/h4&gt;

&lt;p&gt;But that’s it. No other single command stands out, it’s just a lot of small things that add up. Daniel says “it has to take &lt;em&gt;some&lt;/em&gt; time,” and he’s mostly right, but I still have one trick up my sleeve.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;.bashrc&lt;/code&gt; is split into several smaller parts in &lt;code&gt;~/.bashrc.d&lt;/code&gt;, so I can profile these and see if anything stands out. My &lt;a href="https://github.com/liskin/dotfiles/blob/68964611b4b578b646cf5f13a47a4ee77e93e740/.bashrc"&gt;&lt;code&gt;.bashrc&lt;/code&gt;&lt;/a&gt; thus becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; ~/.bashrc.d/&lt;span class="k"&gt;*&lt;/span&gt;.sh&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="nv"&gt;$__bashrc_bench&lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;TIMEFORMAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;: %R"&lt;/span&gt;
        &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nb"&gt;unset &lt;/span&gt;TIMEFORMAT
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;unset &lt;/span&gt;i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s see what happens…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tomi@notes ~]$ __bashrc_bench=1 bash -i
/home/tomi/.bashrc.d/10_env.sh: 0,118
/home/tomi/.bashrc.d/20_history.sh: 0,000
/home/tomi/.bashrc.d/20_prompt.sh: 0,002
/home/tomi/.bashrc.d/30_completion_git.sh: 0,000
/home/tomi/.bashrc.d/31_completion.sh: 0,011
/home/tomi/.bashrc.d/50_aliases.sh: 0,002
/home/tomi/.bashrc.d/50_aliases_sudo.sh: 0,000
/home/tomi/.bashrc.d/50_functions.sh: 0,001
/home/tomi/.bashrc.d/50_git_dotfiles.sh: 0,008
/home/tomi/.bashrc.d/50_mc.sh: 0,000
/home/tomi/.bashrc.d/90_fzf.sh: 0,011
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;118 ms in &lt;code&gt;10_env.sh&lt;/code&gt; was caused by &lt;code&gt;man -w&lt;/code&gt; and we know what to do with that.&lt;/p&gt;

&lt;h4&gt;
  
  
  completions
&lt;/h4&gt;

&lt;p&gt;11 ms in &lt;code&gt;31_completion.sh&lt;/code&gt; which loads &lt;a href="https://github.com/scop/bash-completion"&gt;bash-completion&lt;/a&gt;. That’s certainly better than Daniel’s 235 ms, probably because up-to-date bash-completion only loads a few necessary completions and defers everything else to being loaded on demand. I couldn’t live without the completions, so 11 ms is a fair price.&lt;/p&gt;

&lt;p&gt;8 ms for &lt;code&gt;50_git_dotfiles.sh&lt;/code&gt;, which defines a few aliases and sets up git completions for my &lt;code&gt;git-dotfiles&lt;/code&gt; alias, seems too much, though. Good news is that we don’t need to drop this. We can use bash-completion’s on-demand loading. Whenever completions for command &lt;code&gt;cmd&lt;/code&gt; are needed for the first time, bash-completion looks for &lt;code&gt;~/.local/share/bash-completion/completions/cmd&lt;/code&gt; or &lt;code&gt;/usr/share/bash-completion/completions/cmd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Therefore, &lt;a href="https://github.com/liskin/dotfiles/blob/68964611b4b578b646cf5f13a47a4ee77e93e740/.local/share/bash-completion/completions/git-dotfiles"&gt;&lt;code&gt;~/.local/share/bash-completion/completions/git-dotfiles&lt;/code&gt;&lt;/a&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt; /usr/share/bash-completion/completions/git
&lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; _git git-dotfiles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  fzf
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;90_fzf.sh&lt;/code&gt; loads key bindings and completions code so that &lt;a href="https://github.com/junegunn/fzf"&gt;fzf&lt;/a&gt; is used when searching through history, completing &lt;code&gt;**&lt;/code&gt; in filenames, etc. Well worth the 11 ms it needs to load&lt;sup id="fnref:fzf"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  are we done yet?
&lt;/h4&gt;

&lt;p&gt;After these changes, I got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tomi@notes ~]$ __bashrc_bench=1 bash -i
/home/tomi/.bashrc.d/10_env.sh: 0,001
/home/tomi/.bashrc.d/20_history.sh: 0,000
/home/tomi/.bashrc.d/20_prompt.sh: 0,002
/home/tomi/.bashrc.d/30_completion_git.sh: 0,000
/home/tomi/.bashrc.d/31_completion.sh: 0,012
/home/tomi/.bashrc.d/50_aliases.sh: 0,002
/home/tomi/.bashrc.d/50_aliases_sudo.sh: 0,000
/home/tomi/.bashrc.d/50_functions.sh: 0,001
/home/tomi/.bashrc.d/50_git_dotfiles.sh: 0,000
/home/tomi/.bashrc.d/50_mc.sh: 0,000
/home/tomi/.bashrc.d/90_fzf.sh: 0,011
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s 29 ms, brilliant! Or… is it? 🤔&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tomi@notes ~]$ hyperfine 'bash -i'
Benchmark #1: bash -i
  Time (mean ± σ): 55.7 ms ± 1.0 ms [User: 47.6 ms, System: 11.1 ms]
  Range (min … max): 54.8 ms … 58.9 ms 53 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  history
&lt;/h4&gt;

&lt;p&gt;Some of those additional 26 ms are spent reading my huge (&lt;code&gt;HISTSIZE=50000&lt;/code&gt;) &lt;code&gt;.bash_history&lt;/code&gt; file. I will skip the details about how I investigated this, because I didn’t: I stumbled upon this by chance while testing something else.&lt;/p&gt;

&lt;p&gt;We can see that using an empty history file brings us down to a little under 40 ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tomi@notes ~]$ HISTFILE=/tmp/.bash_history_tmp hyperfine 'bash -i'
Benchmark #1: bash -i
  Time (mean ± σ): 38.6 ms ± 0.7 ms [User: 34.0 ms, System: 7.8 ms]
  Range (min … max): 37.8 ms … 42.3 ms 75 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, cutting 17 ms by sacrificing the shell history is probably not a good deal for most people. I settled for setting up a systemd &lt;a href="https://github.com/liskin/dotfiles/blob/f978be7424946afebe56dbe5ecc85c9f36d1e057/.config/systemd/user/liskin-backup-bash-history.timer"&gt;timer&lt;/a&gt; to &lt;a href="https://github.com/liskin/dotfiles/blob/f978be7424946afebe56dbe5ecc85c9f36d1e057/bin/liskin-backup-bash-history"&gt;back up &lt;code&gt;.bash_history&lt;/code&gt;&lt;/a&gt; to git once a day and lowered &lt;code&gt;HISTSIZE&lt;/code&gt; to 5000&lt;sup id="fnref:history"&gt;3&lt;/sup&gt;. This still keeps my bash startup below 40 ms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[tomi@notes ~]$ hyperfine 'bash -i'
Benchmark #1: bash -i
  Time (mean ± σ): 39.9 ms ± 0.5 ms [User: 36.1 ms, System: 6.8 ms]
  Range (min … max): 39.1 ms … 42.1 ms 73 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;By dropping unnecessary invocation of &lt;code&gt;man -w&lt;/code&gt;, deferring loading of git completions to when they’re needed, and shortening my shell history file, I managed to speed up bash startup from 165 ms to 40 ms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Benchmark #1: bash -i
  Time (mean ± σ): 165.8 ms ± 0.7 ms [User: 156.3 ms, System: 12.8 ms]
  Range (min … max): 164.9 ms … 167.1 ms 17 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Benchmark #1: bash -i
  Time (mean ± σ): 39.9 ms ± 0.5 ms [User: 36.1 ms, System: 6.8 ms]
  Range (min … max): 39.1 ms … 42.1 ms 73 runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More importantly, I no longer type before the prompt, even if I try!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HHAx8LOi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/corrtype.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HHAx8LOi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/corrtype.png" alt="not messed up prompt" width="205" height="36"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And at this point I can finally agree with Daniel that further tweaking will only have diminishing returns&lt;sup id="fnref:latency"&gt;4&lt;/sup&gt;. 😊&lt;/p&gt;




&lt;h3&gt;
  
  
  Up­date 1: Why not fix typ­ing be­fore the prompt in­stead?
&lt;/h3&gt;

&lt;p&gt;Redditor &lt;em&gt;buttellmewhynot&lt;/em&gt; (pun intended) &lt;a href="https://old.reddit.com/r/linux/comments/jxfm2y/even_faster_bash_startup_165_ms_40_ms/gcxiigg/"&gt;comments&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I feel like it shouldn't matter that the shell starts with a delay. If you start a shell, the computer should assume that you want further input directed there and queue somewhere to send it to the shell when it's up.&lt;/p&gt;

&lt;p&gt;I understand that there's probably a lot of weird quirks about how terminals and shells work and how processes get created but surely there's a way to do this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They're right on both points. The input is queued somewhere, and there is a way to fix the messed up prompt. As some might suspect, &lt;a href="https://www.zsh.org/"&gt;zsh&lt;/a&gt; handles it fine: try running &lt;code&gt;sleep 5&lt;/code&gt; and type some input in the meantime:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;zsh&lt;/th&gt;
&lt;th&gt;bash&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Gtr_J9gN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/zsh-nolf.png" alt="zsh no lf" width="210" height="75"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fdUVf30l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/bash-nolf.png" alt="bash no lf" width="210" height="75"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AcLR7pXc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/zsh-lf.png" alt="zsh lf" width="210" height="75"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I7og7vXJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/bash-lf.png" alt="bash lf" width="210" height="75"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
pending input handling without custom prompt



&lt;p&gt;We can see that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in all cases, the input appears twice (bit annoying, but tolerable)&lt;/li&gt;
&lt;li&gt;zsh prompt is never messed up&lt;/li&gt;
&lt;li&gt;bash prompt is messed up if there's no newline after the input&lt;sup id="fnref:readline-assumes"&gt;5&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;no input is discarded, in contrast to the first image of this post&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turns out &lt;a href="https://github.com/liskin/dotfiles/blob/460bdc3c5fa814b874c19d172ce0e3955e278207/.bashrc.d/20_prompt.sh#L13-L27"&gt;my PROMPT_COMMAND&lt;/a&gt; which was meant to &lt;a href="https://stackoverflow.com/q/19943482/3407728"&gt;ensure the prompt always starts on new line&lt;/a&gt; was discarding the pending input. Zsh uses &lt;a href="https://github.com/zsh-users/zsh/blob/19390a1ba8dc983b0a1379058e90cd51ce156815/Src/utils.c#L1599"&gt;a different approach&lt;/a&gt;, printing &lt;code&gt;$COLUMNS&lt;/code&gt; spaces and then a carriage return (&lt;a href="https://serverfault.com/a/97543"&gt;explanation&lt;/a&gt;), which I don't like as it messes up copy/paste. But I &lt;a href="https://github.com/liskin/dotfiles/compare/a5db1831b37f89e00a637bcc20594a4fcf16de1d%5E...322ad36ec1d3ee5485c7b637e4c41fff7ea6745c"&gt;managed to improve my solution&lt;/a&gt; to correctly detect pending input and not discard it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VpeNW6h1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/earlytype.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VpeNW6h1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/even-faster-bash-startup/earlytype.png" alt="not messed up prompt after early typing" width="204" height="50"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's not perfect (so I'll still try to keep bash startup fast), but it's definitely an improvement, and it will be useful whenever I get impatient with a slow command and start typing the next command before the prompt appears.&lt;/p&gt;

&lt;p&gt;Thank you &lt;a href="https://old.reddit.com/user/buttellmewhynot"&gt;&lt;em&gt;buttellmewhynot&lt;/em&gt;&lt;/a&gt; for nudging me in the correct direction.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a id="fn:man-seccomp"&gt;&lt;/a&gt;At the time of publishing this post, &lt;code&gt;man -w&lt;/code&gt; no longer takes 100+ ms thanks to &lt;a href="https://github.com/seccomp/libseccomp/blob/2366f6380198c7af23d145a153ccaa9ba37f9db1/CHANGELOG#L13-L14"&gt;several performance improvements in libseccomp&lt;/a&gt; ↩&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a id="fn:fzf"&gt;&lt;/a&gt;At the time of publishing this post, the latest fzf release (&lt;a href="https://github.com/junegunn/fzf/releases/tag/0.24.3"&gt;0.24.3&lt;/a&gt;) loads twice as long (20+ ms). I fixed this in &lt;a href="https://github.com/junegunn/fzf/pull/2246"&gt;#2246&lt;/a&gt; and &lt;a href="https://github.com/junegunn/fzf/pull/2250"&gt;#2250&lt;/a&gt;, but it might take a short while to be released and find its way to distributions. ↩&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a id="fn:history"&gt;&lt;/a&gt;5000 is a bit limiting in practice, as it rolls over in a few weeks. In 2020, you’d expect your shell to keep &lt;a href="https://superuser.com/questions/137438/how-to-unlimited-bash-shell-history"&gt;unlimited history&lt;/a&gt; without slowdown. I will address this in another post soon. ↩&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a id="fn:latency"&gt;&lt;/a&gt;Some people may be even more sensitive to latency than me, but measurements by &lt;a href="https://danluu.com/"&gt;Dan Luu&lt;/a&gt; suggest that at this scale there are other bottlenecks: &lt;a href="https://danluu.com/input-lag/"&gt;Computer latency&lt;/a&gt;, &lt;a href="https://danluu.com/keyboard-latency/"&gt;Keyboard latency&lt;/a&gt;. ↩&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a id="fn:readline-assumes"&gt;&lt;/a&gt;&lt;a href="https://twobithistory.org/2019/08/22/readline.html"&gt;GNU Readline&lt;/a&gt; assumes the prompt starts in the first column so it gets more messed up later e.g. when walking through history using     ↑/↓. ↩&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>productivity</category>
      <category>performance</category>
    </item>
    <item>
      <title>A better xrandr command-line experience</title>
      <dc:creator>Tomáš Janoušek</dc:creator>
      <pubDate>Sun, 11 Oct 2020 00:00:00 +0000</pubDate>
      <link>https://forem.com/liskin/a-better-xrandr-command-line-experience-21d6</link>
      <guid>https://forem.com/liskin/a-better-xrandr-command-line-experience-21d6</guid>
      <description>&lt;p&gt;Can we make a small improvement to &lt;a href="https://manpages.debian.org/unstable/x11-xserver-utils/xrandr.1.en.html"&gt;xrandr&lt;/a&gt; command-line user experience so that extra tools like &lt;a href="https://christian.amsuess.com/tools/arandr/"&gt;arandr&lt;/a&gt; (GUI for xrandr) or &lt;a href="https://github.com/phillipberndt/autorandr"&gt;autorandr&lt;/a&gt; become unnecessary for some people (like me)? Yes, I think that making the &lt;code&gt;--output&lt;/code&gt; option a bit more powerful goes a long way, and lets me cover most use-cases with just four shell functions/aliases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;function &lt;/span&gt;layout-vertical &lt;span class="o"&gt;{&lt;/span&gt;
  xrandr-smart &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
               &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'!(eDP-*)'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--above&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;layout-horizontal &lt;span class="o"&gt;{&lt;/span&gt;
  xrandr-smart &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
               &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'!(eDP-*)'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--right-of&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;layout-clone &lt;span class="o"&gt;{&lt;/span&gt;
  xrandr-smart &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
               &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'!(eDP-*)'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--same-as&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;layout-extonly &lt;span class="o"&gt;{&lt;/span&gt;
  xrandr-smart &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'!(eDP-*)'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
functions/aliases I wish I could have





&lt;h3&gt;
  
  
  Table of Contents
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;TL;DR&lt;/li&gt;
&lt;li&gt;What? Why?&lt;/li&gt;
&lt;li&gt;Behind the scenes&lt;/li&gt;
&lt;li&gt;Limitations&lt;/li&gt;
&lt;li&gt;Future work&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;Grab it here: &lt;a href="https://github.com/liskin/dotfiles/tree/standalone/xrandr-smart"&gt;release branch&lt;/a&gt;, &lt;a href="https://github.com/liskin/dotfiles/archive/standalone/xrandr-smart.tar.gz"&gt;tarball&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What? Why?
&lt;/h3&gt;

&lt;p&gt;The way monitor layouts work for most people with mainstream operating systems or desktop environments (like &lt;a href="https://www.gnome.org/gnome-3/"&gt;GNOME&lt;/a&gt;) is this: you connect an external monitor for the first time, your desktop expands to this monitor, then you can change its position or turn it off in the settings, and then this setup is remembered so that you don’t need to do it again the next time you connect it. People with non-mainstream &lt;a href="https://en.wikipedia.org/wiki/X_window_manager"&gt;X11 window managers&lt;/a&gt; (like &lt;a href="https://xmonad.org/"&gt;xmonad&lt;/a&gt;, &lt;a href="https://i3wm.org/"&gt;i3&lt;/a&gt;, &lt;a href="https://awesomewm.org/"&gt;awesomewm&lt;/a&gt;, &lt;a href="http://fluxbox.org/"&gt;fluxbox&lt;/a&gt;) can get a similar experience: &lt;a href="https://christian.amsuess.com/tools/arandr/"&gt;arandr&lt;/a&gt; being the “settings UI” and &lt;a href="https://github.com/phillipberndt/autorandr"&gt;autorandr&lt;/a&gt; handling the initial expansion, saving and restoring.&lt;/p&gt;

&lt;p&gt;Still, many people don’t know about these tools, and just use plain xrandr, and then look a bit too nerdy when trying to connect to a projector at some meetup or conference. I’ve got to admit I used to be one of them: I had a script to handle external monitors at home/work, but connecting anything else was so unusual that I didn’t bother writing a script, and had to do it manually, and feel bad about myself afterwards.&lt;/p&gt;

&lt;p&gt;This year I finally decided to do something about it. Not a big deal, right? Just adopt autorandr and be done with it. But the script is massive (1500 lines of code including various workarounds for X11 and driver bugs that are already fixed) and the format of its state/configuration files is undocumented, and that’s a bit of a red flag as I like to keep this stuff cleaned up and in &lt;a href="https://en.wikipedia.org/wiki/Version_control"&gt;version control&lt;/a&gt;. So I tried to think of something simpler.&lt;/p&gt;

&lt;p&gt;The simple solution I came up with is to extend xrandr to &lt;strong&gt;allow &lt;a href="https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html"&gt;shell globs&lt;/a&gt; as output names&lt;/strong&gt; and &lt;strong&gt;disable unspecified outputs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So instead of having several scripts like&lt;/p&gt;
&lt;a href="https://github.com/liskin/dotfiles/blob/ca010423335ae885ff620e60ed37186b12354cc8/bin/layout-home-cz-hdmi"&gt;layout-home-cz-hdmi&lt;/a&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xrandr &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; HDMI-2 &lt;span class="nt"&gt;--mode&lt;/span&gt; 1920x1200 &lt;span class="nt"&gt;--pos&lt;/span&gt; 0x0 &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; eDP-1 &lt;span class="nt"&gt;--mode&lt;/span&gt; 1920x1080 &lt;span class="nt"&gt;--pos&lt;/span&gt; 0x1200 &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="nt"&gt;--primary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;a href="https://github.com/liskin/dotfiles/blob/ca010423335ae885ff620e60ed37186b12354cc8/bin/layout-home-uk-dock"&gt;layout-home-uk-dock&lt;/a&gt;







&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xrandr &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; DP-2-1 &lt;span class="nt"&gt;--mode&lt;/span&gt; 1920x1080 &lt;span class="nt"&gt;--pos&lt;/span&gt; 0x0 &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; eDP-1 &lt;span class="nt"&gt;--mode&lt;/span&gt; 1920x1080 &lt;span class="nt"&gt;--pos&lt;/span&gt; 0x1080 &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="nt"&gt;--primary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;now I just have&lt;/p&gt;
&lt;a href="https://github.com/liskin/dotfiles/blob/74fed5fca5c2f414a588d2e02880c968eb224615/bin/layout-vertical"&gt;layout-vertical&lt;/a&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xrandr-smart &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="nt"&gt;--primary&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; &lt;span class="s1"&gt;'!(eDP-*)'&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--above&lt;/span&gt; &lt;span class="s1"&gt;'eDP-*'&lt;/span&gt; &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and I think that’s beautiful.&lt;/p&gt;

&lt;p&gt;The source for the &lt;code&gt;xrandr-smart&lt;/code&gt; script as described is &lt;a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart"&gt;in my dotfiles monorepo&lt;/a&gt;, but the best way to obtain the most recent version of it is to use the &lt;a href="https://github.com/liskin/dotfiles/tree/standalone/xrandr-smart"&gt;standalone/xrandr-smart branch&lt;/a&gt;, which can also be downloaded as &lt;a href="https://github.com/liskin/dotfiles/archive/standalone/xrandr-smart.tar.gz"&gt;a tarball&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Behind the scenes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart"&gt;&lt;code&gt;xrandr-smart&lt;/code&gt;&lt;/a&gt; invokes the &lt;a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart#L76"&gt;&lt;code&gt;xrandr-auto-find&lt;/code&gt; function&lt;/a&gt; which resolves output globs (if the globs match nothing or more than one output, it fails) and invokes the &lt;a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart#L51"&gt;&lt;code&gt;xrandr-auto-off&lt;/code&gt; function&lt;/a&gt;to disable all other unspecific outputs.&lt;/p&gt;

&lt;p&gt;As an example, &lt;code&gt;layout-vertical&lt;/code&gt; might translate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xrandr-auto-off &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; eDP-1 &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="nt"&gt;--primary&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; HDMI-1 &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--above&lt;/span&gt; eDP-1 &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96
↓
xrandr &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; eDP-1 &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="nt"&gt;--primary&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; HDMI-1 &lt;span class="nt"&gt;--auto&lt;/span&gt; &lt;span class="nt"&gt;--above&lt;/span&gt; eDP-1 &lt;span class="nt"&gt;--dpi&lt;/span&gt; 96 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; DP-1 &lt;span class="nt"&gt;--off&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; DP-2 &lt;span class="nt"&gt;--off&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; HDMI-2 &lt;span class="nt"&gt;--off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s all there is to it, really.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;p&gt;There are two significant limitations compared to &lt;a href="https://www.gnome.org/gnome-3/"&gt;GNOME&lt;/a&gt; and &lt;a href="https://github.com/phillipberndt/autorandr"&gt;autorandr&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The generic &lt;code&gt;layout-horizontal&lt;/code&gt;, &lt;code&gt;layout-vertical&lt;/code&gt; scripts can only support the laptop panel and one external monitor. In reality, this isn’t a problem as triple head setups usual need &lt;a href="https://github.com/liskin/dotfiles/blob/74fed5fca5c2f414a588d2e02880c968eb224615/bin/layout-work2-dock#L6-L8"&gt;fine-tuned positioning&lt;/a&gt; anyway.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We need extra code to support layout saving and (possibly automatic) restoring. Turns out that’s just a few lines: &lt;a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/layout-auto"&gt;&lt;code&gt;layout-auto&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/.xmonad/xmonad.hs#L89-L90"&gt;keybindings&lt;/a&gt;&lt;sup id="fnref:not-auto"&gt;1&lt;/sup&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Future work
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;We could additionally use &lt;a href="https://manpages.debian.org/unstable/read-edid/parse-edid.1.en.html"&gt;parse-edid&lt;/a&gt; to get monitor vendor and model and match against that as well. This could make &lt;code&gt;xrandr-smart&lt;/code&gt; useful even in triple-head and non-laptop setups.&lt;/li&gt;
&lt;/ul&gt;




&lt;ol&gt;
&lt;li&gt;Yeah, I invoke this manually using a Fn-key combo. There’s an endless stream of bugs in the kernel, X server and GPU drivers, plus the occassional security issue in a screensaver, so I feel safer to just invoke it manually when I think everything is settled down. ↩
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>linux</category>
      <category>x11</category>
      <category>xmonad</category>
      <category>bash</category>
    </item>
    <item>
      <title>font-weight: 300 considered harmful (and a fontconfig workaround)</title>
      <dc:creator>Tomáš Janoušek</dc:creator>
      <pubDate>Sat, 18 Jul 2020 00:00:00 +0000</pubDate>
      <link>https://forem.com/liskin/font-weight-300-considered-harmful-and-a-fontconfig-workaround-4ldk</link>
      <guid>https://forem.com/liskin/font-weight-300-considered-harmful-and-a-fontconfig-workaround-4ldk</guid>
      <description>&lt;p&gt;Many web pages these days set &lt;code&gt;font-weight: 300&lt;/code&gt; in their stylesheet. With &lt;a href="https://dejavu-fonts.github.io/"&gt;DejaVu Sans&lt;/a&gt; as my preferred font, this results in very thin and light text that is hard to read, because for some reason the “DejaVu Sans ExtraLight” variant (weight 200) is being used for weights &amp;lt; 360 (in Chrome; in Firefox up to 399). Let’s investigate why this happens and what can be done about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table of Contents
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Problem&lt;/li&gt;
&lt;li&gt;MacOS font smoothing, CSS&lt;/li&gt;
&lt;li&gt;Linux, fontconfig, CSS&lt;/li&gt;
&lt;li&gt;The Solution&lt;/li&gt;
&lt;li&gt;Appendix A: Why glob?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Here’s what &lt;a href="https://work.lisk.in/img/font-weight-300/test.html"&gt;a test page&lt;/a&gt; looks like on my laptop (14” 1920x1080):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://work.lisk.in/img/font-weight-300/test-linux-dejavu.png"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RDPxcEB7--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://work.lisk.in/img/font-weight-300/test-linux-dejavu.png" alt="DejaVu Linux test" width="439" height="301"&gt;&lt;/a&gt;&lt;/p&gt;
DejaVu Sans at different font-weights



&lt;p&gt;For comparison, and possibly also as a clue as to why web designers use &lt;code&gt;font-weight: 300&lt;/code&gt;, here’s a table of various font-weights of DejaVu Sans on my system and the default sans-serif font on MacOS Catalina and Android (unfortunately I don’t have any HiDPI laptop or low-DPI smartphone, so the comparison might be imprecise/unfair):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8EuSPtsR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h7k05f04q3z0cxdg426w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8EuSPtsR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h7k05f04q3z0cxdg426w.png" alt="Boldness comparison" width="416" height="111"&gt;&lt;/a&gt;&lt;/p&gt;
Boldness comparison&lt;sup id="fnref:screenshots"&gt;1&lt;/sup&gt; (scaled to equal
height)



&lt;h3&gt;
  
  
  MacOS font smoothing, CSS
&lt;/h3&gt;

&lt;p&gt;In MacOS, &lt;code&gt;font-weight: normal&lt;/code&gt; looks almost bold, so web designers who use MacOS/Safari might use &lt;code&gt;font-weight: 300&lt;/code&gt; to &lt;a href="https://news.ycombinator.com/item?id=23553486"&gt;compensate for this, ruining it for everybody else&lt;/a&gt;. :-(&lt;/p&gt;

&lt;p&gt;Well, actually not everybody, as some desktop users (e.g. a Fedora Live DVD) won’t have an extra-light variant of sans serif, so the normal (regular, or book) variant will be used for all weights. But Android users and desktop users with DejaVu (used to be default on most Linux distributions, not sure what’s the current status) and possibly also Windows users are affected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://tonsky.me/blog/monitors/#turn-off-font-smoothing"&gt;Nikita Prokopov suggested that disabling font smoothing in MacOS reduces the boldness&lt;/a&gt;, and my experiments confirm that. Furthermore, subpixel smoothing (antialiasing)&lt;sup id="fnref:macos-subpixel"&gt;2&lt;/sup&gt; comes somewhere in the middle between the default and no smoothing (on my display).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZPfPcdIP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2hyyqera9hqafckjd5cz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZPfPcdIP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2hyyqera9hqafckjd5cz.png" alt="Effect of disabling font smoothing in MacOS" width="439" height="102"&gt;&lt;/a&gt;&lt;/p&gt;
Effect of disabling font smoothing in MacOS



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YBFGdRbB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/sju23aka9qnl2m1z6bdq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YBFGdRbB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/sju23aka9qnl2m1z6bdq.png" alt="Boldness comparison, this time with no smoothing&amp;lt;br&amp;gt;
in MacOS" width="416" height="111"&gt;&lt;/a&gt;&lt;/p&gt;
Boldness comparison, this time with no smoothing
in MacOS



&lt;p&gt;&lt;del&gt;Anyway, we can’t put all the blame on web designers. Matching an extra-light font with &lt;code&gt;font-weight: 300&lt;/code&gt; doesn’t seem to be a good idea, and matching it with &lt;code&gt;font-weight: 350&lt;/code&gt; is just plain silly (and I’d need to use explicit language to describe my feelings about Firefox using an extra-light font for &lt;code&gt;font-weight: 399&lt;/code&gt;).&lt;/del&gt;&lt;/p&gt;

&lt;p&gt;Actually, we can put all the blame on them, as &lt;code&gt;font-weight: 300&lt;/code&gt; has always (&lt;a href="https://www.w3.org/TR/CSS1/#font-weight"&gt;even in CSS Level 1&lt;/a&gt;) meant “lighter than normal, even if the only lighter font is weight 100.” Firefox’s behaviour of selecting an extra-light font for &lt;code&gt;font-weight: 399&lt;/code&gt; is in fact &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights"&gt;conforming to the most recent draft specification&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MacOS’ somewhat bolder rendering of normal-weight fonts is therefore a &lt;em&gt;very&lt;/em&gt; weak excuse for using &lt;code&gt;font-weight: 300&lt;/code&gt;, which literally forces the browser to not use a normal-weight font (or bolder) unless there is no other font available.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With that out of the way, let’s finally proceed to &lt;del&gt;fix&lt;/del&gt; work around the problem, since persuading thousands of web developers to fix their websites doesn’t seem feasible at this point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Linux, fontconfig, CSS
&lt;/h3&gt;

&lt;p&gt;Font selection and appearance in Linux is &lt;a href="https://wiki.archlinux.org/index.php/Font_configuration"&gt;highly&lt;/a&gt; &lt;a href="https://wiki.archlinux.org/index.php/Font_configuration/Examples"&gt;configurable&lt;/a&gt; via &lt;a href="https://www.freedesktop.org/software/fontconfig/fontconfig-user.html"&gt;fontconfig&lt;/a&gt;. That is both a curse and a blessing. In this case, it is quite advantageous.&lt;/p&gt;

&lt;p&gt;There are a few handy command-line utilities which make it really easy to test the configuration. I’ll use &lt;a href="https://linux.die.net/man/1/fc-list"&gt;fc-list&lt;/a&gt; and &lt;a href="https://linux.die.net/man/1/fc-match"&gt;fc-match&lt;/a&gt; here to see what fonts I have and when DejaVu Sans ExtraLight is used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ fc-list | grep -F -w 'DejaVu Sans' | sort
/usr/share/fonts/truetype/dejavu/DejaVuSans-BoldOblique.ttf: DejaVu Sans:style=Bold Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-BoldOblique.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed Bold Oblique,Bold Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Bold.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed Bold,Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Oblique.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed Oblique,Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed,Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-ExtraLight.ttf: DejaVu Sans,DejaVu Sans Light:style=ExtraLight
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-BoldOblique.ttf: DejaVu Sans Mono:style=Bold Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Oblique.ttf: DejaVu Sans Mono:style=Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf: DejaVu Sans:style=Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book


$ fc-match -v sans \
  | grep -F -w -e style: -e weight: -e fullname:
        style: "Book"(s)
        fullname: "DejaVu Sans"(s)
        weight: 80(f)(s)

$ fc-match -v sans:weight=extralight \
  | grep -F -w -e style: -e weight: -e fullname:
        style: "ExtraLight"(s)
        fullname: "DejaVu Sans ExtraLight"(s)
        weight: 40(f)(s)

$ fc-match -v sans:weight=60 | grep -F -w -e weight: 
        weight: 40(f)(s)

$ fc-match -v sans:weight=61 | grep -F -w -e weight: 
        weight: 80(f)(s)

$ fc-match -v sans:weight=139 | grep -F -w -e weight: 
        weight: 80(f)(s)

$ fc-match -v sans:weight=140 | grep -F -w -e weight: 
        weight: 200(f)(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fontconfig defines these symbolic font weights:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;constant&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;thin&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extralight&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ultralight&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;light&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;demilight&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;semilight&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;book&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;regular&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;normal&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;demibold&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;semibold&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bold&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extrabold&lt;/td&gt;
&lt;td&gt;205&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;black&lt;/td&gt;
&lt;td&gt;210&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;heavy&lt;/td&gt;
&lt;td&gt;210&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
Fontconfig weight constants



&lt;p&gt;Apparently fontconfig selects the font with the closest weight requested. That’s not what &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights"&gt;CSS needs&lt;/a&gt;, so browsers probably don’t use fontconfig font patterns and therefore &lt;a href="https://old.reddit.com/r/linuxquestions/comments/a4h90n/using_fontconfig_to_block_a_problematic_font/"&gt;the usual fontconfig ways of avoiding the extra-light font don’t work.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But wait. Actually, some browsers do. The &lt;a href="https://surf.suckless.org/"&gt;surf&lt;/a&gt; browser, built using &lt;a href="https://webkitgtk.org/"&gt;WebKitGTK&lt;/a&gt;, translates &lt;code&gt;font-weigth: 300&lt;/code&gt; to fontconfig weight 50, &lt;code&gt;font-weight: 200&lt;/code&gt; to fontconfig weight 40 and &lt;code&gt;font-weight: 100&lt;/code&gt; to fontconfig weight 0, which is a correct mapping, but it won’t result in correct behaviour if only font weights 0 and 80 are available, as 80 is closer to 60, but CSS mandates that 0 is chosen. (To find this out, I used &lt;code&gt;FC_DEBUG=1 surf&lt;/code&gt;.) Indeed, the fontconfig configuration suggested in the link above is a sufficient workaround for the &lt;a href="https://webkitgtk.org/"&gt;WebKitGTK&lt;/a&gt; browser:&lt;/p&gt;
surf before


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ FC_DEBUG=1 surf test.html |&amp;amp;amp; grep -F -w -c ExtraLight
7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
~/.config/fontconfig/fonts.conf




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?xml version="1.0"?&amp;gt;
&amp;lt;!DOCTYPE fontconfig SYSTEM "fonts.dtd"&amp;gt;
&amp;lt;fontconfig&amp;gt;
    &amp;lt;match target="pattern"&amp;gt;
        &amp;lt;test qual="any" name="family"&amp;gt;
            &amp;lt;string&amp;gt;DejaVu Sans&amp;lt;/string&amp;gt;
        &amp;lt;/test&amp;gt;
        &amp;lt;test name="weight" compare="less"&amp;gt;
            &amp;lt;const&amp;gt;book&amp;lt;/const&amp;gt;
        &amp;lt;/test&amp;gt;
        &amp;lt;edit name="weight" mode="assign" binding="same"&amp;gt;
            &amp;lt;const&amp;gt;book&amp;lt;/const&amp;gt;
        &amp;lt;/edit&amp;gt;
    &amp;lt;/match&amp;gt;
&amp;lt;/fontconfig&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
surf after




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ FC_DEBUG=1 surf test.html |&amp;amp;amp; grep -F -w -c ExtraLight
0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;In a real CSS-conforming browser, this won’t work as fontconfig is presumably only used to list available fonts, and the font matching algorithm then runs in the browser engine itself. One might also desperately attempt to use fontconfig’s &lt;code&gt;&amp;lt;match target="scan"&amp;gt;&lt;/code&gt; to lower the weight of the font to 0 and hope the browser will select the nearer, normal variant. Or at least I did desperately try that. That won’t work, either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;CSS still prefers a weight 0 font for &lt;code&gt;font-weight: 300&lt;/code&gt; when both weight 0 and weight 400 are available.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;&amp;lt;match target="scan"&amp;gt;&lt;/code&gt; needs to be applied system-wide and fontconfig caches then need to be regenerated using &lt;a href="https://linux.die.net/man/1/fc-cache"&gt;fc-cache&lt;/a&gt; by root, as apparently the system-wide caches are preferred. Therefore it’s also impossible to apply this rule to a web browser only.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is still one option left, fortunately: &lt;code&gt;&amp;lt;selectfont&amp;gt;&lt;/code&gt;, which controls the set of available fonts. Its documentation is quite high-level and in some aspects downright incorrect, but by reading &lt;a href="https://github.com/freedesktop/fontconfig/blob/437f03299bd1adc9673cd576072f1657be8fd4e0/src/fccfg.c#L461-L478"&gt;the source&lt;/a&gt; we can conclude that it works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;First, check if the filename is explicitly accepted by any &lt;code&gt;&amp;lt;glob&amp;gt;&lt;/code&gt;. If it isn’t, then check whether it’s rejected, and only if it’s not accepted but it is explicitly rejected, skip the font. Otherwise continue.&lt;br&gt;&lt;br&gt;
(The documentation claims that &lt;code&gt;&amp;lt;glob&amp;gt;&lt;/code&gt; only filters directories, but this is fortunately not true.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then, similarly, check if the font matches any accept &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; (these may test various font properties). If not, check reject patterns, and skip the font if rejected and not accepted. Otherwise continue and allow the font to be used.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Order of configuration directives doesn’t matter, it’s just being added to glob/pattern accept/reject lists as the configuration is read.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;Fontconfig’s &lt;code&gt;&amp;lt;selectfont&amp;gt;&lt;/code&gt; lets us hide DejaVu Sans ExtraLight from the browser. If we want to keep the font available for other applications (if we don’t, then it might be easier to just uninstall it), let’s create a browser-specific fontconfig conf:&lt;/p&gt;
~/.config/fontconfig/browser.conf





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?xml version="1.0"?&amp;gt;
&amp;lt;!DOCTYPE fontconfig SYSTEM "fonts.dtd"&amp;gt;
&amp;lt;fontconfig&amp;gt;
    &amp;lt;include&amp;gt;fonts.conf&amp;lt;/include&amp;gt;

    &amp;lt;!-- disable DejaVu Sans ExtraLight, it tends to match font-weight: 300 --&amp;gt;
    &amp;lt;selectfont&amp;gt;
        &amp;lt;rejectfont&amp;gt;
            &amp;lt;glob&amp;gt;*/DejaVuSans-ExtraLight.ttf&amp;lt;/glob&amp;gt;
        &amp;lt;/rejectfont&amp;gt;
    &amp;lt;/selectfont&amp;gt;
&amp;lt;/fontconfig&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we now set the &lt;code&gt;FONTCONFIG_FILE=~/.config/fontconfig/browser.conf&lt;/code&gt; environment variable, DejaVu Sans ExtraLight is nowhere to be seen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ FONTCONFIG_FILE=~/.config/fontconfig/browser.conf \
  fc-match -v sans:weight=40 | grep -F -w -e weight:
        weight: 80(f)(s)

$ FONTCONFIG_FILE=~/.config/fontconfig/browser.conf \
  fc-list | grep -F -w -c ExtraLight
0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting &lt;code&gt;FONTCONFIG_FILE=~/.config/fontconfig/browser.conf&lt;/code&gt; for the browser is left as an exercise to the reader.&lt;/p&gt;

&lt;h3&gt;
  
  
  Appendix A: Why glob?
&lt;/h3&gt;

&lt;p&gt;An observant reader might have noticed that the solution could be made more robust by using &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;glob&amp;gt;&lt;/code&gt; and matching on the font weight, thus disabling all light fonts. This is probably correct, but not usable in my case, as I already use accept patterns to &lt;a href="https://github.com/liskin/dotfiles/blob/3d30f7f25b6a30ec8216ed370efd88fb35f6f080/.config/fontconfig/browser.conf#L6-L62"&gt;limit the available fonts to a few reasonable ones&lt;/a&gt; to prevent web designers from selecting hard to read font faces. With the advent of web fonts, this workaround has become less effective lately. :-(&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a id="fn:screenshots"&gt;&lt;/a&gt;Screenshots:&lt;br&gt;&lt;br&gt;
&lt;a href="https://work.lisk.in/img/font-weight-300/test-linux-dejavu.png"&gt;DejaVu Sans on my system&lt;/a&gt;&lt;br&gt;
&lt;a href="https://work.lisk.in/img/font-weight-300/test-macos.png"&gt;MacOS Catalina&lt;/a&gt;&lt;br&gt;
&lt;a href="https://work.lisk.in/img/font-weight-300/test-macos-subpixel.png"&gt;MacOS Catalina + subpixel antialiasing&lt;/a&gt;&lt;br&gt;
&lt;a href="https://work.lisk.in/img/font-weight-300/test-macos-nosmooth.png"&gt;MacOS Catalina + disabled font smoothing&lt;/a&gt;&lt;br&gt;
&lt;a href="https://work.lisk.in/img/font-weight-300/test-android.png"&gt;Android Samsung S10e&lt;/a&gt;&lt;br&gt;
&lt;a href="https://work.lisk.in/img/font-weight-300/test-android-i9300.png"&gt;Android Samsung S3&lt;/a&gt;&lt;br&gt;&lt;br&gt;
↩&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a id="fn:macos-subpixel"&gt;&lt;/a&gt;Subpixel antialiasing is &lt;a href="https://apple.stackexchange.com/questions/337870/how-to-turn-subpixel-antialiasing-on-in-macos-10-14"&gt;disabled since Mojave&lt;/a&gt;, possibly because it’s not necessary with HiDPI/Retina displays and dropping it reduces code complexity considerably. ↩&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>css</category>
    </item>
  </channel>
</rss>
