<?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: Simon Wicki</title>
    <description>The latest articles on Forem by Simon Wicki (@zwacky).</description>
    <link>https://forem.com/zwacky</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%2F277593%2F29add9f7-4fe8-4608-8838-490dbeeb660c.jpg</url>
      <title>Forem: Simon Wicki</title>
      <link>https://forem.com/zwacky</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zwacky"/>
    <language>en</language>
    <item>
      <title>The Duality of CLS with Lazy Loading Components</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Mon, 28 Mar 2022 06:29:15 +0000</pubDate>
      <link>https://forem.com/zwacky/the-duality-of-cls-with-lazy-loading-components-1e5g</link>
      <guid>https://forem.com/zwacky/the-duality-of-cls-with-lazy-loading-components-1e5g</guid>
      <description>&lt;p&gt;When you optimise your web app, your goal is to make the experience better for the user: That means usually 'faster' by transferring and parsing less data. But caution: The same web app can cause Cumulative Layout Shift (CLS) on slower connections but runs without CLS on faster connection.&lt;/p&gt;

&lt;p&gt;If you'd like a refresher about Core Web Vitals, I explained them with GIFs in &lt;a href="https://wicki.io/posts/2021-07-core-web-vitals/"&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; slower connections can result in CLS when lazy loading components that you wouldn't see on wifi connections.&lt;/p&gt;

&lt;p&gt;Either don't lazy load the component at all or await for the js file to be loaded and mounted.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Duality
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZgZRBQV5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o7m95b2fj1gmdq8r47dk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZgZRBQV5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o7m95b2fj1gmdq8r47dk.gif" alt="Slow vs fast connection: Same web app with different CLS." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We assume that a web app loads the same on slower connections, just slower. Unfortunately that's not always the case with lazy loadable components.&lt;/p&gt;

&lt;p&gt;With lazy loadable components we deal with two asyncnesses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;async API responses (JSON)&lt;/li&gt;
&lt;li&gt;async lazy loading components (JS)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What if the API responses (1) are faster than the dynamically loaded JS (2)? What if you lazy load a component that sits in the middle of your web content? The answer to these questions you see in the screencapture above: Google will punish you with CLS.&lt;/p&gt;




&lt;h2&gt;
  
  
  CLS measurement
&lt;/h2&gt;

&lt;p&gt;I've seen a lot of confusion about Core Web Vitals, especially CLS.&lt;/p&gt;

&lt;p&gt;Unlike other Core Web Vitals, CLS is continuously measured and cumulatively added to the score. For a classic SPA web app this means that Google will keep the CLS score on a per-route basis.&lt;/p&gt;

&lt;p&gt;CLS has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After each route change, the CLS resets to 0&lt;/li&gt;
&lt;li&gt;After any user interaction, you get a grace period of 500ms where CLS is not taken into account&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Measurements by real users:&lt;/strong&gt; Chrome users send Core Web Vitals metrics to Google directly. It's not a Googlebot that captures these metrics while crawling the site.&lt;/p&gt;

&lt;p&gt;These real user measurements are collected as Field Data and flow into Google's CrUX report.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means you need to take the real world into account:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you have a lot of traffic from India, but your servers are on the other end of the world, that your LCP (Largest Contentful Paint) will probably suffer.&lt;/li&gt;
&lt;li&gt;If you have a lot of traffic from China, it's likely that some services are blocked by Chinese ISPs and won't load. This could cause unwanted CLS within your content.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Solutions
&lt;/h2&gt;

&lt;p&gt;We need to be in full control of what to display to the user at what time. With duality in mind, we need to know the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;are API requests still loading?&lt;/li&gt;
&lt;li&gt;are async components components still loading?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A skeleton loader is an ideal way to wait until both, API requests and async components, are ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution #1: Don't lazy load components at all
&lt;/h3&gt;

&lt;p&gt;The quickest and least error prone solution would be to pass on lazy loading components. In most cases the saved kilobytes through lazy loading doesn't justify the CLS that it might cause. If your performance budget allows it, go with this solution.&lt;/p&gt;

&lt;p&gt;Let's assume we have a web app with 10% logged-in users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// not all users require to download and render HugeComponent&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HugeComponent&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without code splitting we'd send the JS of &lt;code&gt;&amp;lt;HugeComponent&amp;gt;&lt;/code&gt; to 90% of the users that don't need it. This can affect LCP and FID.&lt;/p&gt;

&lt;p&gt;With code splitting we'd pack the JS of &lt;code&gt;&amp;lt;HugeComponent&amp;gt;&lt;/code&gt; into the additional-comps.js chunk and only send it over the wire when it's needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// without code splitting&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HugeComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/HugeComponent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// with code splitting&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HugeComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="cm"&gt;/* webpackChunkName: "additional-comps" */&lt;/span&gt; 
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/HugeComponent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two criterias that help you decide if you should lazy load a component or not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;size of &lt;/li&gt;
&lt;li&gt;how often  would be rendered for users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you come to the conclusion that your performance budget is tight and you need to lazy load the component, see solution #2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution #2: Await loading and mounting of component
&lt;/h3&gt;

&lt;p&gt;If your component isn't used for the majority of users and it would increase the bundle by a lot, have a look at this solution.&lt;/p&gt;

&lt;p&gt;Wait for async components to be lazily loaded and mounted can be tricky. You need to render the component but the mounting happens later. Here's a gist of how it could be done.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Parent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// important: only hide the children with display: none.&lt;/span&gt;
    &lt;span class="c1"&gt;// if we used if-else, we'd never load the lazy loadable component;&lt;/span&gt;
    &lt;span class="c1"&gt;// and then we'd never change isLoading with its callback.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* HugeComponent is lazy loaded, because not all users will need it */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HugeComponent&lt;/span&gt; 
            &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt; 
            &lt;span class="na"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;)&lt;/span&gt;

        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* display skeleton element for time of loading */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"skeleton-loader"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;HugeComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dataA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// tell the parent component that everything is ready to be fully rendered&lt;/span&gt;
        &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we didn't use &lt;code&gt;display: hidden&lt;/code&gt; for the loading state on the &lt;code&gt;&amp;lt;HugeComponent&amp;gt;&lt;/code&gt;, we'd never trigger the loading of the async component. Thus, Line 28 would never be reached and the &lt;code&gt;isLoading&lt;/code&gt; state would stay on &lt;code&gt;false&lt;/code&gt; forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When you lost green URLs in the &lt;a href="https://search.google.com/search-console"&gt;Google Search Console&lt;/a&gt; due to CLS and you can't reproduce it yourself, try debugging your web app with a slower connection.&lt;/p&gt;

&lt;p&gt;So if you're using lazy loadable components, chances are high that might be a victim of the duality of CLS.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>cwv</category>
      <category>cls</category>
    </item>
    <item>
      <title>Sued by Using Google Fonts: Data Privacy and GDPR</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Tue, 22 Feb 2022 09:59:49 +0000</pubDate>
      <link>https://forem.com/zwacky/time-to-say-goodbye-to-google-fonts-data-privacy-and-gdpr-55mm</link>
      <guid>https://forem.com/zwacky/time-to-say-goodbye-to-google-fonts-data-privacy-and-gdpr-55mm</guid>
      <description>&lt;p&gt;The German court &lt;a href="https://rewis.io/urteile/urteil/lhm-20-01-2022-3-o-1749320/"&gt;has ruled&lt;/a&gt; last month that Google Fonts is not in compliance with GDPR.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The integration of dynamic web content such as Google Fonts from US web services is illegal without the consent of the visitor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A website operator received a fine of 100€. The Munich court clearly wanted to set an example. They even mentioned the next fine will be 250.000€ for the website operator if they don't comply.&lt;/p&gt;

&lt;p&gt;Data protection authorities (DPA) in other EU countries became all ears. It's likely to see more rulings and enforcements of this in the name of GDPR.&lt;/p&gt;

&lt;p&gt;In this post I want to show why you should care, even if you're not from Germany.&lt;/p&gt;




&lt;h1&gt;
  
  
  How Google Fonts collects personal data
&lt;/h1&gt;

&lt;p&gt;When a user wants to load a font via Google Fonts, it uses 2 types of requests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dynamic request: &lt;code&gt;fonts.googleapis.com/css2?family={font}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Asset request(s): &lt;code&gt;fonts.gstatic.com/s/{font}/...&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fhgz-1CU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/90wtaa73t5ki12o3paqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fhgz-1CU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/90wtaa73t5ki12o3paqo.png" alt="Requests needed for Google Fonts" width="880" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dynamic request is the reason of the German court's ruling: The user's IP address is shared with Google Fonts. This is object to personally identifiable information (PII).&lt;/p&gt;

&lt;p&gt;From the Google Fonts &lt;a href="https://developers.google.com/fonts/faq#what_does_using_the_google_fonts_api_mean_for_the_privacy_of_my_users"&gt;FAQ&lt;/a&gt; we get a fuzzy idea of what is going on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Google Fonts logs records of the CSS and the font file requests, and access to this data is kept secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Are they logging user's IP addresses along other fingerprinting PIIs?&lt;/li&gt;
&lt;li&gt;Are they using these records to fill the gaps of a user's internet journey?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What we know is that they collect and store end-user data to be able to—what they state as needed to "serve fonts efficiently".&lt;/p&gt;

&lt;h1&gt;
  
  
  "I'm running Google Fonts, what are my options?"
&lt;/h1&gt;

&lt;p&gt;From a GDPR point of view you have 2 options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Host the fonts locally&lt;/strong&gt;: You can already download them from the Google Fonts website directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep using it and ask your users for consent&lt;/strong&gt;: Because of the data transfer to Google you're required to ask your users for their consent. Implement a Consent Banner where Google Fonts is stated as one of the Data Processing Services (DPS). On top, you need to await your user's consent before requesting the Google Font service. Postponing font files is inherently not ideal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Just ask yourself if the additional value of Google Fonts' convenience is worth it to pay the price for data privacy.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why you should care
&lt;/h1&gt;

&lt;p&gt;When tech companies get enough data points to connect the dots they get a pretty good picture of what you do on the internet. This data is usually used for personalised advertisement. I could live with this if it was the user’s only drawback of personal data collection.&lt;/p&gt;

&lt;p&gt;Google Fonts is one of these data points that helps connecting further puzzle pieces together.&lt;/p&gt;

&lt;p&gt;Besides the advertisement aspect, it’s problematic because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the lack of control over your personal data&lt;/li&gt;
&lt;li&gt;leaving a profile on tech companies' servers that can be accessed under certain requirements by the US authorities&lt;/li&gt;
&lt;li&gt;being on the internet where data leaks and hacks happen very often&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Currently it’s up to us developers to protect the end-user. This is why you should care.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Closing words
&lt;/h1&gt;

&lt;p&gt;I hope I could shed some light on why Google Fonts is data privacy concern.&lt;/p&gt;

&lt;p&gt;Also: I am not a lawyer.&lt;/p&gt;

</description>
      <category>googlefonts</category>
      <category>gdpr</category>
      <category>dataprivacy</category>
    </item>
    <item>
      <title>An Ode to AngularJS</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Wed, 19 Jan 2022 09:14:26 +0000</pubDate>
      <link>https://forem.com/zwacky/an-ode-to-angularjs-3l61</link>
      <guid>https://forem.com/zwacky/an-ode-to-angularjs-3l61</guid>
      <description>&lt;p&gt;In remembrance of my first love.&lt;/p&gt;

&lt;p&gt;Your LTS came finally to an end. You retired on 31st December 2021. You will always have a special place in my heart.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;You arrived with great anticipation,&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    Put the web dev world upside down;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Model, View and Controller were in separation,&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    Even Javascript beginners didn’t frown.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Be it JavaScript, CoffeeScript or SCSS,&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    You handled them all in a snap;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Grunt helped you to transpile and compress,&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    The only thing needed was an &lt;code&gt;ng-app&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You were patient with faulty code,&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    Throwing errors with the where and why;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Did selectors return a wrong DOM node?&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    Did watchers kill the CPU workload?&lt;/em&gt;&lt;br&gt;
&lt;em&gt;        Did a variable on &lt;code&gt;$scope&lt;/code&gt; get overshadowed?&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Maybe the external callback needs an &lt;code&gt;$apply&lt;/code&gt;!&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    Maybe &lt;code&gt;$inject&lt;/code&gt; lacks used params to minify!&lt;/em&gt;&lt;br&gt;
&lt;em&gt;        Maybe avoid &lt;code&gt;$rootScope&lt;/code&gt; for all events passing by!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You were either praised or attacked,&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    Yet, you advanced and left doubters behind;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Daily flame wars you fought against react,&lt;/em&gt;&lt;br&gt;
&lt;em&gt;    Until the day &lt;code&gt;window.angular&lt;/code&gt; became &lt;code&gt;undefined&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;If you found this post interesting please leave a ❤️ on this tweet and consider following my 🎢 journey about #webperf, #buildinpublic, #nonfiction books and #frontend things &lt;a href="https://twitter.com/zwacky"&gt;on Twitter&lt;/a&gt;.&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--WAkAxzpN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/991612191991312386/DrWCwHbi_normal.jpg" alt="Simon Wicki profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Simon Wicki
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        &lt;a class="mentioned-user" href="https://dev.to/zwacky"&gt;@zwacky&lt;/a&gt;
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ir1kO05j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      An Ode to AngularJS&lt;br&gt;&lt;br&gt;You arrived with great anticipation,&lt;br&gt;    Put the web dev world upside down;&lt;br&gt;Model, View and Controller were in separation,&lt;br&gt;    Even Javascript beginners didn’t frown.
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      10:18 AM - 19 Jan 2022
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1483745787632828423" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fFnoeFxk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1483745787632828423" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6dcrOn8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1483745787632828423" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SRQc9lOp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


</description>
      <category>angular</category>
    </item>
    <item>
      <title>How does your company approach mentorship?</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Fri, 07 Jan 2022 14:31:40 +0000</pubDate>
      <link>https://forem.com/zwacky/how-does-your-company-approach-mentorship-28g5</link>
      <guid>https://forem.com/zwacky/how-does-your-company-approach-mentorship-28g5</guid>
      <description>&lt;p&gt;How did you get there?&lt;br&gt;
How did you form the best mentorship model?&lt;br&gt;
Are there any frameworks that can be implemented like agile?&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--WAkAxzpN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/991612191991312386/DrWCwHbi_normal.jpg" alt="Simon Wicki profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Simon Wicki
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        &lt;a class="mentioned-user" href="https://dev.to/zwacky"&gt;@zwacky&lt;/a&gt;
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ir1kO05j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      How does your company approach mentorship? &lt;a href="https://twitter.com/hashtag/devcommunity"&gt;#devcommunity&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      14:29 PM - 07 Jan 2022
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1479460148305444867" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fFnoeFxk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1479460148305444867" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6dcrOn8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1479460148305444867" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SRQc9lOp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


</description>
    </item>
    <item>
      <title>Interface vs Type Alias in TypeScript—Quick Comparison</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Fri, 07 Jan 2022 07:56:22 +0000</pubDate>
      <link>https://forem.com/zwacky/interface-vs-type-alias-in-typescript-quick-comparison-2g37</link>
      <guid>https://forem.com/zwacky/interface-vs-type-alias-in-typescript-quick-comparison-2g37</guid>
      <description>&lt;p&gt;This can be confusing for anyone working with TypeScript—beginner or seasoned programmer. Both ways can cover similar needs.&lt;/p&gt;

&lt;p&gt;In this post you’ll quickly see what Typescript feature is possible to implement as Type Alias or Interface. I stayed brief and spicy to give the post an overview character.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;TLDR&lt;/strong&gt;: You don’t care about each difference? What should you use?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use interface until you need type&lt;br&gt;
—&lt;a href="https://twitter.com/orta/status/1356129195835973632" rel="noopener noreferrer"&gt;orta&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Overview: Interface (I) vs Type Alias (T)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;I&lt;/th&gt;
&lt;th&gt;T&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primitives&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type UUID = string&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extend / Intersect&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Response &amp;amp; ErrorHandling&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unions&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string | number&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mapped object types&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;['apple' | 'orange']: number&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Augment existing types&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;declare global { interface Window { … } }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declare type with typeof&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Response = typeof ReturnType&amp;lt;fetch&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tuples&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[string, number]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Functions&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(x: number, y: number): void&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursion&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Nested { children?: Nested[] }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Primitives
&lt;/h2&gt;

&lt;p&gt;❌: Interface&lt;br&gt;
✅: Type Alias&lt;/p&gt;

&lt;p&gt;&lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt; and &lt;code&gt;boolean&lt;/code&gt; make up the Primitives in Typescript.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq69vvz3u4c3gwwe9mtav.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq69vvz3u4c3gwwe9mtav.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Extend / Intersect
&lt;/h2&gt;

&lt;p&gt;✅: Interface&lt;br&gt;
✅: Type Alias&lt;/p&gt;

&lt;p&gt;Although intersect and extend are not 100% the same for interface and type alias, I put them together in this example. The differences arise when type keys appear in both types that you want to extend or intersect from.&lt;/p&gt;

&lt;p&gt;So if the extended or intersected key is not the same type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type Alias lets you do it but changes that type to &lt;code&gt;never&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Interface spits an error that the types are not compatible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4aaw9v00gtgky7jtk7y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs4aaw9v00gtgky7jtk7y.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Mapped object types
&lt;/h2&gt;

&lt;p&gt;❌: Interface&lt;br&gt;
✅: Type Alias&lt;/p&gt;

&lt;p&gt;Type the keys in your objects with this.&lt;/p&gt;

&lt;p&gt;Here are a few useful applications of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[key: string]&lt;/code&gt;: only strings as key allowed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[key: number]&lt;/code&gt;: only numbers as key allowed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[key in keyof T as `get${Capitalize&amp;lt;string &amp;amp; key&amp;gt;}`]&lt;/code&gt;: only allow keys that start with &lt;code&gt;get...&lt;/code&gt;, e.g. as seen in a Getter object&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vqybmzkih1urhy46zro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vqybmzkih1urhy46zro.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Unions
&lt;/h2&gt;

&lt;p&gt;❌: Interface&lt;br&gt;
✅: Type Alias&lt;/p&gt;

&lt;p&gt;Typescript’s equivalent to &lt;code&gt;OR&lt;/code&gt;: The type is either &lt;code&gt;x&lt;/code&gt; or &lt;code&gt;y&lt;/code&gt; or &lt;code&gt;z&lt;/code&gt; or as many as you want.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l2dger256xqufv9pry7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l2dger256xqufv9pry7.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Augment existing types
&lt;/h2&gt;

&lt;p&gt;✅: Interface&lt;br&gt;
❌: Type Alias&lt;/p&gt;

&lt;p&gt;You can add fields to already existing types. It’s useful when you add a new field (e.g. &lt;code&gt;jQuery&lt;/code&gt; library for auto completion) onto an existing type (e.g. &lt;code&gt;window&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrw13zxmqojvsbrznglq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrw13zxmqojvsbrznglq.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Tuples
&lt;/h2&gt;

&lt;p&gt;✅: Interface&lt;br&gt;
✅: Type Alias&lt;/p&gt;

&lt;p&gt;If you’ve used hooks in react, then you know the usefulness of tuples.&lt;/p&gt;

&lt;p&gt;A single function call can return an array of values and functions, that are destructured and can be used as fully typed variables: &lt;code&gt;const [name, setName] = useState('')&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfndojgot452cppipeko.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfndojgot452cppipeko.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Functions
&lt;/h2&gt;

&lt;p&gt;✅: Interface&lt;br&gt;
✅: Type Alias&lt;/p&gt;

&lt;p&gt;Functions can be annotated with Parameter Types and Return Types.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ewt8rhsj7jkc0cfmntn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ewt8rhsj7jkc0cfmntn.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Recursion
&lt;/h2&gt;

&lt;p&gt;✅: Interface&lt;br&gt;
✅: Type Alias&lt;/p&gt;

&lt;p&gt;Recursion are simple to use. Make sure you add the optional &lt;code&gt;?&lt;/code&gt; to the recursive property. Otherwise the TS compiler spits out an error upon searching an endless recursion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0l61a0p49jrwbakemcf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0l61a0p49jrwbakemcf.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  More resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.typescriptlang.org/play" rel="noopener noreferrer"&gt;Typescript Playground&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/typescript-cheatsheets/react#types-or-interfaces" rel="noopener noreferrer"&gt;Types or Interfaces in react&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stackoverflow.com/a/65948871/825444" rel="noopener noreferrer"&gt;Interfaces vs Types in TypeScript&lt;/a&gt; by Mark&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.logrocket.com/types-vs-interfaces-in-typescript/" rel="noopener noreferrer"&gt;Types vs. interfaces in TypeScript&lt;/a&gt; by Leonardo Maldonado&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@martin_hotell/interface-vs-type-alias-in-typescript-2-7-2a8f1777af4c" rel="noopener noreferrer"&gt;Interface vs Type alias in TypeScript 2.7&lt;/a&gt; by Martin Hochel&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;If you found this post interesting please leave a ❤️ on this tweet and consider following my 🎢 journey about #webperf, #buildinpublic and #frontend matters &lt;a href="https://twitter.com/zwacky" rel="noopener noreferrer"&gt;on Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1480863714454687746-808" src="https://platform.twitter.com/embed/Tweet.html?id=1480863714454687746"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1480863714454687746-808');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1480863714454687746&amp;amp;theme=dark"
  }



&lt;/p&gt;




&lt;p&gt;Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf &amp;amp; non-fiction books.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://twitter.com/zwacky" rel="noopener noreferrer"&gt;Join me on Twitter to follow my latest updates.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
    </item>
    <item>
      <title>Show Latest Blog Posts in Twitter Header with GitHub Actions</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Tue, 30 Nov 2021 11:16:31 +0000</pubDate>
      <link>https://forem.com/zwacky/show-latest-blog-posts-in-twitter-header-with-github-actions-2n3j</link>
      <guid>https://forem.com/zwacky/show-latest-blog-posts-in-twitter-header-with-github-actions-2n3j</guid>
      <description>&lt;p&gt;I like writing about my dev journey.&lt;/p&gt;

&lt;p&gt;Wouldn't it be great if you could show off your latest blog posts in my Twitter header automatically?&lt;/p&gt;

&lt;p&gt;This way you could better showcase what you're about. People visiting your Twitter profile will know right away if they should or shouldn't leave a follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  My Workflow
&lt;/h3&gt;

&lt;p&gt;My GitHub &lt;a href="https://github.com/zwacky/zwacky/blob/main/.github/workflows/twitter-header-blog-posts-workflow.yml"&gt;workflow&lt;/a&gt; that uses my GitHub &lt;a href="https://github.com/zwacky/twitter-header-blog-posts-action"&gt;action&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;twitter-header-blog-posts-action&lt;/code&gt; GitHub Action is responsible for doing the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch last 3 blog posts from your blog&lt;/li&gt;
&lt;li&gt;Draw fetched blog posts titles onto header&lt;/li&gt;
&lt;li&gt;Draw any text onto header&lt;/li&gt;
&lt;li&gt;Draw any images onto header&lt;/li&gt;
&lt;li&gt;Use Twitter API to upload your dynamic banner&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Submission Category:
&lt;/h3&gt;

&lt;p&gt;Maintainer Must-Haves / Wacky Wildcards&lt;/p&gt;

&lt;h3&gt;
  
  
  Yaml File or Link to Code
&lt;/h3&gt;

&lt;p&gt;In this posts cover image you see my Twitter header that I created with this action. Checkout the following workflow.yml I used to create it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Twitter header with latest blog posts&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Run workflow automatically&lt;/span&gt;
    &lt;span class="c1"&gt;# This will make it run twice a day (06:00 and 18:00)&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0 18 * * *&lt;/span&gt;
    &lt;span class="c1"&gt;# Run workflow manually (without waiting for the cron to be called), through the Github Actions Workflow page directly&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;udpate-twitter-blog-posts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update Twitter header with latest blog posts&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zwacky/twitter-header-blog-posts-action@main&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;BLOG_RSS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://dev.to/feed/zwacky&lt;/span&gt;
          &lt;span class="na"&gt;DRAW_TEXTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[["h1",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Hey,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I'&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Simon",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;125,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;100],&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;["h1",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;blog&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;posts",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;835,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;70],&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;["h1",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Follow&lt;/span&gt;&lt;span class="nv"&gt;   &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;say&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hi!",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;545,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;390],&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;["p",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tweet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;about&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;webperf,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;javascript,",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;125,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;177],&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;["p",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;learnings&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;books.",&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;125,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;212]]'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;TWITTER_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TWITTER_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;TWITTER_API_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TWITTER_API_SECRET }}&lt;/span&gt;
          &lt;span class="na"&gt;TWITTER_ACCESS_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TWITTER_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;TWITTER_ACCESS_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TWITTER_ACCESS_SECRET }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;a href="https://github.com/zwacky/twitter-header-blog-posts-action#how-to-use"&gt;how to use&lt;/a&gt; section to get a step-by-step guide how to obtain Twitter API credentials and more.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://github.com/zwacky/twitter-header-blog-posts-action#inputs"&gt;input docs&lt;/a&gt; to see what customisation is available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Additional Resources / Info
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/zwacky/goodreads-profile-workflow"&gt;goodreads-profile-workflow&lt;/a&gt;: Another GitHub Action I created—also for dev bloggers!&lt;/li&gt;
&lt;li&gt;Check my Twitter &lt;a href="https://twitter.com/zwacky"&gt;@zwacky&lt;/a&gt; to see the result!&lt;/li&gt;
&lt;li&gt;Check out &lt;a href="https://dev.to/erikaheidi/dynamic-twitter-header-images-with-dynacover-github-action-35nb"&gt;Erika's action&lt;/a&gt; that lets you show your latest followers in your Twitter header—Sweet!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>actionshackathon21</category>
      <category>githubactions</category>
      <category>node</category>
      <category>twitter</category>
    </item>
    <item>
      <title>One Font Format to Rule Them All: WOFF2</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Thu, 11 Nov 2021 07:19:38 +0000</pubDate>
      <link>https://forem.com/zwacky/one-font-format-to-rule-them-all-woff2-2jgn</link>
      <guid>https://forem.com/zwacky/one-font-format-to-rule-them-all-woff2-2jgn</guid>
      <description>&lt;p&gt;Learn why WOFF2, and WOFF as a fallback, is the only font format you need.&lt;/p&gt;

&lt;p&gt;If you’ve been around as a web developer for some time, you’ve seen these font formats in your CSS. Here’s an example for a self-hosted Raleway font:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Raleway'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('../fonts/raleway-v22-latin-regular.eot')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* IE9 Compat Modes */&lt;/span&gt;
  &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="sx"&gt;url('../fonts/raleway-v22-latin-regular.eot?#iefix')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'embedded-opentype'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;/* IE6-IE8 */&lt;/span&gt;
       &lt;span class="sx"&gt;url('../fonts/raleway-v22-latin-regular.woff2')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'woff2'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;/* Super Modern Browsers */&lt;/span&gt;
       &lt;span class="sx"&gt;url('../fonts/raleway-v22-latin-regular.woff')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'woff'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;/* Modern Browsers */&lt;/span&gt;
       &lt;span class="sx"&gt;url('../fonts/raleway-v22-latin-regular.ttf')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'truetype'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;/* Safari, Android, iOS */&lt;/span&gt;
       &lt;span class="sx"&gt;url('../fonts/raleway-v22-latin-regular.svg#Raleway')&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'svg'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* Legacy iOS */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do we still need to support all 5 (!) of them? Where did they come from? How do they compare to each other?&lt;/p&gt;

&lt;p&gt;These questions I’d like to answer in this post.&lt;/p&gt;




&lt;h1&gt;
  
  
  Timeline
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--I9uEWwnn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kyl9zgw4to0b17djb0x9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I9uEWwnn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kyl9zgw4to0b17djb0x9.png" alt="Image description" width="880" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It took time and iterations to have what we’ve ended up with.&lt;/p&gt;

&lt;p&gt;With more efficient font formats, thanks to compression, browsers were able to support them.&lt;/p&gt;

&lt;p&gt;Have a look at the elaborate &lt;a href="https://typeforge.files.wordpress.com/2011/11/timeline_formatos_software_fontes_v10-01.jpg"&gt;Font Format Timeline&lt;/a&gt; by Pedro Amado. This shows, that there are in fact many more facets to font formats than we think.&lt;/p&gt;




&lt;h1&gt;
  
  
  Comparison
&lt;/h1&gt;

&lt;p&gt;I’ll go ahead and use the following criteria for my score to compare font formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser support as of November 2021&lt;/li&gt;
&lt;li&gt;File size (aka format efficiency)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the file size I used &lt;a href="http://comicneue.com/"&gt;Comic Sans Neue&lt;/a&gt; and the browser support I gathered from &lt;a href="https://caniuse.com/"&gt;caniuse.com&lt;/a&gt;. Here’s the let’s have a look at the following table ordered historically with these criteria:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Font Format&lt;/th&gt;
&lt;th&gt;Release year&lt;/th&gt;
&lt;th&gt;Browser support&lt;/th&gt;
&lt;th&gt;File size&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Score&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OTF/TTF&lt;/td&gt;
&lt;td&gt;1985&lt;/td&gt;
&lt;td&gt;98.6%&lt;/td&gt;
&lt;td&gt;57 kb&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69/100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SVG&lt;/td&gt;
&lt;td&gt;2001&lt;/td&gt;
&lt;td&gt;19.4%&lt;/td&gt;
&lt;td&gt;113 kb&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20/100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EOT&lt;/td&gt;
&lt;td&gt;2007&lt;/td&gt;
&lt;td&gt;0.87%&lt;/td&gt;
&lt;td&gt;27 kb&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;43/100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WOFF&lt;/td&gt;
&lt;td&gt;2009&lt;/td&gt;
&lt;td&gt;98.6%&lt;/td&gt;
&lt;td&gt;30 kb&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88/100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WOFF2&lt;/td&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;96.8%&lt;/td&gt;
&lt;td&gt;23 kb&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98/100&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let me explain why WOFF2 scored higher than WOFF in this comparison: It all comes down to the 7 kb in file size. Generally if your site shows the user content more quickly, your conversion will increase—even if the gain is only in the hundred millisecond range.&lt;/p&gt;

&lt;p&gt;Your fonts could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;blocking content from appearing (invisible text)&lt;/li&gt;
&lt;li&gt;cause layout shifts&lt;/li&gt;
&lt;li&gt;delay further assets from downloading on flaky mobile networks&lt;/li&gt;
&lt;li&gt;sending bad signals to Google (LCP &amp;amp; CLS from Core Web Vitals)&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  How to convert
&lt;/h1&gt;

&lt;p&gt;It's a very good idea to self host your fonts for performance reasons. I described one case in my &lt;a href="https://wicki.io/posts/2020-11-goodbye-google-fonts/"&gt;Time to Say Goodbye to Google Fonts&lt;/a&gt;&lt;br&gt;
post.&lt;/p&gt;

&lt;p&gt;How to best convert a font type to WOFF2 and WOFF depends where you got your font type from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google Fonts:&lt;/strong&gt; Download the used font types on Google Fonts and use &lt;a href="http://google-webfonts-helper.herokuapp.com/fonts"&gt;google-webfonts-helper&lt;/a&gt; to download the other font formats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;everything else:&lt;/strong&gt; Use &lt;a href="https://www.fontsquirrel.com/tools/webfont-generator"&gt;Font Squirrels's Webfont Generator&lt;/a&gt;. The 'optimal' setting even only spits out WOFF2 and WOFF files.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;WOFF2, and WOFF as fallback, is enough nowadays¹.&lt;/p&gt;

&lt;p&gt;¹) &lt;em&gt;if&lt;/em&gt; your site doesn't need support for antiquated browsers like IE8.&lt;/p&gt;




&lt;p&gt;Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf &amp;amp; non-fiction books.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://twitter.com/zwacky"&gt;Join me on Twitter to follow my latest updates.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>Keep Your Javascript Bundle Size in Check</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Wed, 18 Aug 2021 07:34:55 +0000</pubDate>
      <link>https://forem.com/zwacky/keep-your-javascript-bundle-size-in-check-2dd4</link>
      <guid>https://forem.com/zwacky/keep-your-javascript-bundle-size-in-check-2dd4</guid>
      <description>&lt;p&gt;Are you a developer who is concerned about the size of newly added libraries? Or do you want to find a culprit in a rather big Javascript bundle?&lt;/p&gt;

&lt;p&gt;If you’re like me, then you answered yes to both questions.&lt;/p&gt;

&lt;p&gt;In this post I’ll cover a few tools that come in handy for a &lt;strong&gt;quick&lt;/strong&gt; analysis of bundle sizes &lt;strong&gt;without changing or ejecting your build architecture&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  VS Code extension: Import Cost
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZaRoPYY---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1hftfho2vrbbdj4xi163.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZaRoPYY---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1hftfho2vrbbdj4xi163.gif" alt="import-cost" width="838" height="146"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Understand the cost of an import early.&lt;/p&gt;

&lt;p&gt;This extension will display inline in the editor the size of the imported package. It supports tree shaking, so the size should be displayed correctly for a few exported functions.&lt;/p&gt;

&lt;p&gt;With this you may spot mistakes like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;moment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 289.7KB&lt;/span&gt;
&lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;moment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0.082KB&lt;/span&gt;
&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s also available for &lt;strong&gt;JetBrains IDE&lt;/strong&gt;, &lt;strong&gt;Atom&lt;/strong&gt; and &lt;strong&gt;Vim&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/wix/import-cost"&gt;https://github.com/wix/import-cost&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Website: Bundlephobia
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XILBmyrr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e8ru8f41bmh4qzsebzx4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XILBmyrr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e8ru8f41bmh4qzsebzx4.png" alt="bundlephobia-moment" width="880" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;his website lets you search for libraries and display their sizes without the need to install. It shows the size of each version and even suggests alternatives to similar libraries that might be lighter—talking about a new framework or library every week.&lt;/p&gt;

&lt;p&gt;You could also drop your package.json file and order it by size to see your biggest libraries. Personally I find this quite fun, but usually I use this tool to check bundle sizes of not-yet-installed libraries.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://bundlephobia.com/"&gt;https://bundlephobia.com/&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  NPM: source-map-explorer
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tdJpStFl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1bkw43aghv1b170nm2qm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tdJpStFl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1bkw43aghv1b170nm2qm.png" alt="source-map-explorer-example" width="880" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Like the name suggests you need to build source maps. With modern framework CLIs it’s enabled by default in prod builds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Useful tool for imported package visualisation in relation to their size. By clicking on the packages, you can further imspect their sizes and children.&lt;/p&gt;

&lt;p&gt;👉 &lt;code&gt;npx source-map-explorer ./dist *.js&lt;/code&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/danvk/source-map-explorer"&gt;https://github.com/danvk/source-map-explorer&lt;/a&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  Website: PageSpeed Insight / Lighthouse
&lt;/h1&gt;

&lt;p&gt;If your site is already public you can use Google’s PageSpeed Insight to detect big Javascript bundles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus&lt;/strong&gt;: It also includes Javascript files, that are downloaded on runtime from your ad networks, Google Tag Manager and other tools.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://developers.google.com/speed/pagespeed/insights/"&gt;https://developers.google.com/speed/pagespeed/insights/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check out this tweet to see the treemap in action:&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;
      &lt;div class="ltag__twitter-tweet__media ltag__twitter-tweet__media__video-wrapper"&gt;
        &lt;div class="ltag__twitter-tweet__media--video-preview"&gt;
          &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wiydxX3F--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/ext_tw_video_thumb/1400527107952939011/pu/img/LFXnjOWk4o-ZEjst.jpg" alt="unknown tweet media content"&gt;
          &lt;img src="/assets/play-butt.svg" class="ltag__twitter-tweet__play-butt" alt="Play butt"&gt;
        &lt;/div&gt;
        &lt;div class="ltag__twitter-tweet__video"&gt;
          
            
          
        &lt;/div&gt;
      &lt;/div&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://res.cloudinary.com/practicaldev/image/fetch/s--Km6Vpo5l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pbs.twimg.com/profile_images/1174459912945946624/-g0exujd_normal.png" alt="Lighthouse profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Lighthouse
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @____lighthouse
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ir1kO05j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      The Lighthouse Treemap lets you see what's in the JavaScript you ship and find where you can trim based on module size and execution coverage.&lt;br&gt;&lt;br&gt;With Lighthouse 8 it's now in PSI, Canary DevTools, and the CLI if it can see your source maps 🌳🗺️&lt;br&gt;&lt;br&gt;Try it out: &lt;a href="https://t.co/TkkzvxHAbY"&gt;googlechrome.github.io/lighthouse/tre…&lt;/a&gt; 
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      19:08 PM - 03 Jun 2021
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1400529906577162243" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fFnoeFxk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1400529906577162243" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6dcrOn8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1400529906577162243" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SRQc9lOp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;





&lt;p&gt;Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf &amp;amp; non-fiction books.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://twitter.com/zwacky"&gt;Join me on Twitter to follow my latest updates.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>javascript</category>
      <category>webpack</category>
    </item>
    <item>
      <title>Accurate Daily Measurements of Core Web Vitals with Google Analytics</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Tue, 10 Aug 2021 09:01:01 +0000</pubDate>
      <link>https://forem.com/zwacky/accurate-daily-measurements-of-core-web-vitals-with-google-analytics-2pnp</link>
      <guid>https://forem.com/zwacky/accurate-daily-measurements-of-core-web-vitals-with-google-analytics-2pnp</guid>
      <description>&lt;p&gt;Have you ever optimised your website for the Core Web Vitals (CWV)? Did you want to check your changes the next day—but Google's various tools don't give you current daily CWV metrics due to the &lt;em&gt;rolling 28-day window&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;In this post I'll review Google's toolset for measuring CWV and explain how to see if your changes had any effect on CWV day by day with Google Analytics (GA).&lt;/p&gt;

&lt;h1&gt;
  
  
  Accurate daily CWV measurements: Create your report
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Dzhffizi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jzdfoyn2roaj6ahhxn6d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Dzhffizi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jzdfoyn2roaj6ahhxn6d.png" alt="daily-cwv-measurements-workflow-cut" width="880" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There isn't a prebuilt view for daily accurate reports in any analytics service out of the box. That's why we need to create our own report in three steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/GoogleChrome/web-vitals"&gt;web-vitals&lt;/a&gt;: to measure CWV metrics in the frontend&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/GoogleChrome/web-vitals#using-analyticsjs"&gt;CWV GA tracking snippet&lt;/a&gt;: to send these captured metrics over to GA&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web-vitals-report.web.app/"&gt;Web Vitals Report&lt;/a&gt;: to create your CWV report by connecting your GA with this external reporting tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The web-vitals library lets us grab all CWV metrics of each visitor of the site. These metrics are then sent off to Google Analytics. The external Web Vitals Report tool can then extract these tracking events from your GA account and create a daily accurate report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't be fooled&lt;/strong&gt;: GA can display your CLS, LCP and other CWV metrics when you search for these events. But they're inaccurate in the GA events view. For instance it's possible that multiple CLS events are sent, each with their own delta value that accumulates to the total CLS. You'd need to group by that day &lt;strong&gt;and&lt;/strong&gt; url &lt;strong&gt;and&lt;/strong&gt; user to get an accurate reporting. This grouping isn't supported out of the box.&lt;/p&gt;

&lt;p&gt;That's why Google created the external tool &lt;strong&gt;Web Vitals Report&lt;/strong&gt;. You can connect your GA account with it and it will extract the data. If you followed the default naming inside of the CWV GA tracking snippet, everything will work out of the box. You'll be rewarded with shiny graphs and an accurate* report overall.&lt;/p&gt;

&lt;p&gt;*) GA &lt;a href="https://github.com/GoogleChromeLabs/web-vitals-report#1-million-row-limit"&gt;imposes a limit&lt;/a&gt; in each report which can lead to sampling and an inaccurate report. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--orWeBoIz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6zaov2oqaaycbkn7qsjc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--orWeBoIz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6zaov2oqaaycbkn7qsjc.png" alt="report-all" width="880" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot above is an excerpt of the Web Vitals Report with daily accurate Core Web Vital metrics.&lt;/p&gt;




&lt;h1&gt;
  
  
  Inaccurate daily CWV measurements: Google's tools
&lt;/h1&gt;

&lt;p&gt;For daily CWV measurements we can't rely on Google's tools because they either aggregate over a long 28-day period or only measure single pages with synthetic, non Real-User Metrics (RUM). So it takes a lot of days to see any change within that period.&lt;/p&gt;

&lt;p&gt;Still, Google provides a great—and very easy—overview of your CWV metrics to plan where to optimise next. But you can't check the impact on CWV of your optimisation with these tools, e.g. the next day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field data&lt;/strong&gt; aka real-user experiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://search.google.com/search-console"&gt;Google Search Console&lt;/a&gt;: measures similar pages&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/speed/pagespeed/insights/"&gt;PageSpeed Insights&lt;/a&gt;: measures a single page &amp;amp; all pages as a whole&lt;/li&gt;
&lt;li&gt;CrUX [Chrome UX Report): measures single pages &amp;amp; all pages as a whole&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lab data&lt;/strong&gt; aka synthetic user experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/web/tools/lighthouse"&gt;Lighthouse&lt;/a&gt;: measures a single page&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/web/tools/chrome-devtools"&gt;Chrome DevTools&lt;/a&gt;: measures a single page&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chrome.google.com/webstore/detail/web-vitals/ahfhijdlegdabablpippeagghigmibma?hl=en"&gt;Web Vitals Chrome Extension&lt;/a&gt;: measures a single page&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Rolling 28-day window explained
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;I dedicate a paragraph to this in particular because the rolling 28-day window appears many times in CWV measurement—especially in how the field data is measured.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine you had &lt;strong&gt;27 days&lt;/strong&gt; of awful Cumulative Layout Shift (CLS) of 1.0 and on &lt;strong&gt;day 28&lt;/strong&gt; you magically fix it to 0.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;day 28&lt;/strong&gt; your average CLS would be 0.96—which would be rounded up to 1.0 again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Days      | 1    2    3    4    5    6    7    8    9    10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28
-------------------------------------------------------------------------------------------------------------------------------------------------------
Daily CLS | 1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  0.0 
Average   | 1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  1.0  0.96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example above your 1.0 to 0 CLS optimisation would take &lt;strong&gt;+23 days&lt;/strong&gt; until you reach the allowed mobile CLS of 0.1.&lt;/p&gt;

&lt;p&gt;See in the following table how slowly the average is decreasing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Days      | +1   +2   +3   +4   +5   +6   +7   +8   +9   +10  +11  +12  +13  +14  +15  +16  +17  +18  +19  +20  +21  +22  +23  +24  +25  +26  +27  +28
-------------------------------------------------------------------------------------------------------------------------------------------------------
Daily CLS | 0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0 
Average   | 0.9  0.8  0.8  0.8  0.8  0.8  0.7  0.7  0.6  0.6  0.6  0.5  0.5  0.5  0.4  0.4  0.4  0.3  0.3  0.3  0.2  0.2  0.1  0.1  0.1  0.0  0.0  0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  More resources
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.smashingmagazine.com/2021/04/complete-guide-measure-core-web-vitals/"&gt;An In-Depth Guide To Measuring Core Web Vitals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://groups.google.com/a/chromium.org/g/chrome-ux-report/c/PRGtZJvmGkw/m/rzQV99-kCAAJ"&gt;Google Forum: Explanation different data points in different tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tamethebots.com/blog-n-bits/monitoring-search-console-core-web-vitals"&gt;Tracking Changes in Search Console's Ore Web Vitals Report&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf &amp;amp; non-fiction books.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://twitter.com/zwacky"&gt;Join me on Twitter to follow my latest updates.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>google</category>
      <category>performance</category>
      <category>seo</category>
    </item>
    <item>
      <title>How List Rendering Can Cause Huge Cumulative Layout Shift</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Wed, 28 Jul 2021 15:55:57 +0000</pubDate>
      <link>https://forem.com/zwacky/how-list-rendering-can-cause-huge-cumulative-layout-shift-5ckf</link>
      <guid>https://forem.com/zwacky/how-list-rendering-can-cause-huge-cumulative-layout-shift-5ckf</guid>
      <description>&lt;p&gt;Do you...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;care about Core Web Vitals, especially Cumulative Layout Shift (CLS)&lt;/li&gt;
&lt;li&gt;use list rendering with a JS Framework (&lt;code&gt;v-for&lt;/code&gt;, &lt;code&gt;*ngFor&lt;/code&gt;, ...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you answered both with yes, then please read on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TxTyNePu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9nqaa33ozyiguxik84b6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TxTyNePu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9nqaa33ozyiguxik84b6.gif" alt="update-list-cls" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JS frameworks—such as Vue, Angular or React—can cache DOM nodes of list items*. Filtering a list will therefore be faster, because DOM nodes of list items can potentially be reused. This reuse is done by moving the DOM nodes around, instead of re-creating them.&lt;/p&gt;

&lt;p&gt;But when a list changes and the nodes of list items merely switch their positions, &lt;strong&gt;the reused items are considered as a shift in the DOM (CLS) by Core Web Vitals on slow connection devices&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;*) Vue uses &lt;code&gt;:key&lt;/code&gt;, React uses &lt;code&gt;key&lt;/code&gt;, Angular uses &lt;code&gt;*ngFor&lt;/code&gt; with &lt;code&gt;trackBy&lt;/code&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Why only on slow connection devices?
&lt;/h1&gt;

&lt;p&gt;CLS takes user interaction into account. When content is changing after a click, it's not counted towards the CLS until a grace period of 500ms. If the delay of content changing takes longer than 500ms, you'll be penalised with the full CLS score. &lt;/p&gt;

&lt;p&gt;Fast connection devices will usually finish updating the list before the grace period ends.&lt;/p&gt;

&lt;p&gt;Keep an eye out in your Google Search Console for URLs that are frequented by countries that are further away from your servers/CDNs and have a lower average mobile bandwidth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--lwEuDz22--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2pamtctg0jjnyljrdqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--lwEuDz22--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2pamtctg0jjnyljrdqp.png" alt="gsc-urls-affected-by-lower-bandwith" width="880" height="351"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Solution
&lt;/h1&gt;

&lt;p&gt;Google's calculation of CLS &lt;a href="https://web.dev/evolving-cls/"&gt;has been changed before&lt;/a&gt;, so I would love to see this here too. Until then it's on us to not get penalised.&lt;/p&gt;

&lt;p&gt;We'd have to make sure, that the DOM elements get re-created every time a list gets updated or filtered. I wouldn't advise to completely drop the unique key (&lt;code&gt;:key&lt;/code&gt;). Rather think of a key for each list item that is tied to the request itself, e.g. &lt;code&gt;:key="item.id + "-" + requestQuery"&lt;/code&gt;. The filtering itself will have a brief flash due to recreating the DOM node, but considering the Green URLs that Google will give you it's worth it. &lt;/p&gt;

&lt;p&gt;With this you can continue with infinite scroll and page navigation without perf loss.&lt;/p&gt;




&lt;p&gt;Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf &amp;amp; non-fiction books.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://twitter.com/zwacky"&gt;Join me on Twitter to follow my latest updates.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>javascript</category>
      <category>corewebvitals</category>
    </item>
    <item>
      <title>Core Web Vitals explained with GIFs</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Wed, 21 Jul 2021 05:39:05 +0000</pubDate>
      <link>https://forem.com/zwacky/what-are-core-web-vitals-3cc1</link>
      <guid>https://forem.com/zwacky/what-are-core-web-vitals-3cc1</guid>
      <description>&lt;p&gt;With Google's June 2021 update Core Web Vitals (CWV) will become a factor in SEO ranking.&lt;br&gt;
It measures the quality of a site by these three metrics: &lt;strong&gt;LCP&lt;/strong&gt;, &lt;strong&gt;FID&lt;/strong&gt; and &lt;strong&gt;CLS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you pass all of them them, Google will reward you with more visibility.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9BLB-kbK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0n8uj2ns89l4eserilx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9BLB-kbK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x0n8uj2ns89l4eserilx.png" alt="core-web-vitals-green-urls" width="880" height="495"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;You can check how well you do on these metrics via several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/speed/pagespeed/insights/"&gt;Pagespeed Insights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://search.google.com/"&gt;Search Console&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://web.dev/measure/"&gt;WebDev Measure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Chrome DevTools &amp;amp; Lighthouse&lt;/li&gt;
&lt;li&gt;&lt;a href="https://chrome.google.com/webstore/detail/web-vitals/ahfhijdlegdabablpippeagghigmibma?hl=en"&gt;Web Vitals Chrome Extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics"&gt;Custom event tracking with Google Analytics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Largest Contentful Paint (LCP)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;LCP marks the point in the page load timeline when the page's main content has likely loaded.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://web.dev/lcp/"&gt;web.dev&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DOX2GAH8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rx9b94x6rjqwp7fqzvqw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DOX2GAH8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rx9b94x6rjqwp7fqzvqw.gif" alt="LCP-techcrunch" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The faster you make the biggest content on the above-the-fold appear, the better your metric will be. &lt;/p&gt;

&lt;p&gt;If you can preload the biggest image already, e.g. with &lt;code&gt;&amp;lt;link rel="preload" href="..."&amp;gt;&lt;/code&gt; or make the content appear without rendering it with your Javascript Framework, you'll win big.&lt;/p&gt;

&lt;p&gt;Causes of LCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slow server response times&lt;/li&gt;
&lt;li&gt;Render-blocking Javascript and CSS&lt;/li&gt;
&lt;li&gt;Slow resource load times&lt;/li&gt;
&lt;li&gt;Client-side rendering&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  First Input Delay (FID)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;FID measures the time from when a user first interacts with a page to the time when the browser is actually able to begin processing event handlers in response to that interaction.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://web.dev/fid/"&gt;web.dev&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--xQnG01zW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dkkc6vaeukynwebrf0xu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xQnG01zW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dkkc6vaeukynwebrf0xu.gif" alt="FID-imdb" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Javascript is synchronous and single-threaded. If a user interaction happens while the thread is busy, the user has to wait. If you have too many libraries that load at runtime, worse even if you don't need them right away, this can increase the FID.&lt;/p&gt;

&lt;p&gt;It's similar to &lt;a href="https://web.dev/tti/"&gt;TTI (Time to Interactive)&lt;/a&gt; that you might have heard of already. However FID will only start to be measured from the user interaction. Whereas TTI will be measured from the very start of loading the site.&lt;/p&gt;

&lt;p&gt;Causes of FID:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;too much javascript code to execute right away, especially 3rd party&lt;/li&gt;
&lt;li&gt;ad networks can push lots of unneeded Javascript&lt;/li&gt;
&lt;li&gt;not using code-splitting (e.g. with &lt;a href="https://webpack.js.org/guides/code-splitting/"&gt;webpack&lt;/a&gt;, &lt;a href="https://rollupjs.org/guide/en/#code-splitting"&gt;Rollup&lt;/a&gt;, &lt;a href="https://parceljs.org/code_splitting.html"&gt;Parcel&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cumulative Layout Shift (CLS)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;CLS is a measure of the largest burst of layout shift scores for every unexpected layout shift that occurs during the entire lifespan of a page.&lt;/p&gt;

&lt;p&gt;—&lt;a href="https://web.dev/cls/"&gt;web.dev&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--S3zRIkgZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3spmoid4hduxk995c9p7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--S3zRIkgZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3spmoid4hduxk995c9p7.gif" alt="CLS-giphy" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Layout shifts only happen when an element higher up is making other elements move. If the element is changing and there is nothing after it, it doesn't count as a layout shift—as well as DOM changes that were caused by a user interaction with a grace period (500ms).&lt;/p&gt;

&lt;p&gt;Note: Mobile viewports and 3G devices will cause much more CLS. Make sure to throttle your connection while optimising your page for these metrics.&lt;/p&gt;

&lt;p&gt;Causes of CLS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;images without size attributes&lt;/li&gt;
&lt;li&gt;requests finish later that will inject content above existing content&lt;/li&gt;
&lt;li&gt;lazy loaded components&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf &amp;amp; non-fiction books.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://twitter.com/zwacky"&gt;Join me on Twitter to follow my latest updates.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webperf</category>
      <category>seo</category>
      <category>corewebvitals</category>
    </item>
    <item>
      <title>Pimp Your GitHub Profile with Books You Read</title>
      <dc:creator>Simon Wicki</dc:creator>
      <pubDate>Sun, 11 Apr 2021 10:18:57 +0000</pubDate>
      <link>https://forem.com/zwacky/pimp-your-github-profile-with-books-you-read-2l1i</link>
      <guid>https://forem.com/zwacky/pimp-your-github-profile-with-books-you-read-2l1i</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwicki.io%2Fposts%2F2021-04-goodreads-workflow-for-github-actions%2Fgoodreads-github-profile-update-v2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwicki.io%2Fposts%2F2021-04-goodreads-workflow-for-github-actions%2Fgoodreads-github-profile-update-v2.png" alt="Keep the books you read in sync with your GitHub profile README"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post I'd like to show you how to use GitHub Actions to automatically sync your Goodreads books you read in your GitHub profile README.&lt;/p&gt;

&lt;p&gt;I created goodreads-profile-workflow for devs that love to read and like to share what they read. You can customise the input parameters to your liking: list the books you're currently reading, last 5 books you read and even add your personal ratings, too.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/zwacky" rel="noopener noreferrer"&gt;
        zwacky
      &lt;/a&gt; / &lt;a href="https://github.com/zwacky/goodreads-profile-workflow" rel="noopener noreferrer"&gt;
        goodreads-profile-workflow
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Show what you're currently reading and recently read on your GitHub profile readme.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Leave a star on the repo if you think it's cool!&lt;/p&gt;




&lt;h2&gt;
  
  
  GitHub Actions and Profile READMEs
&lt;/h2&gt;

&lt;p&gt;When you head over to your GitHub profile page, &lt;a href="https://github.com/zwacky" rel="noopener noreferrer"&gt;https://github.com/zwacky&lt;/a&gt; in my case, you'll see that you can do more than just pin repositories. If you're missing yours, check out the &lt;a href="https://docs.github.com/en/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme" rel="noopener noreferrer"&gt;official docs on GitHub&lt;/a&gt; to get one, too.&lt;/p&gt;

&lt;p&gt;Keeping your profile README fresh with up-to-date data is a great way to show your visitors what you're up to or what you've been working on. Either you do it manually or you automate it with the help of GitHub Actions. They can run workflows periodically and update your README for you.&lt;/p&gt;

&lt;p&gt;There are tons of great ones out there. Have a look at the great list &lt;a href="https://github.com/abhisheknaiidu/awesome-github-profile-readme" rel="noopener noreferrer"&gt;awesome-github-profile-readme&lt;/a&gt; repo for inspiration. They include GitHub Actions that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;update your latest blog entries&lt;/li&gt;
&lt;li&gt;update your GitHub activity&lt;/li&gt;
&lt;li&gt;or even update your sleep data from your sleep tracker&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How To Use
&lt;/h2&gt;

&lt;p&gt;I'll quickly explain here how to use &lt;code&gt;goodreads-profile-workflow&lt;/code&gt; and what the GitHub Actions do. For a full step-by-step guide read the &lt;a href="https://github.com/zwacky/goodreads-profile-workflow#how-to-use" rel="noopener noreferrer"&gt;how to use section&lt;/a&gt; of the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  #1 Add placeholders to your README
&lt;/h3&gt;

&lt;p&gt;Your README.md will be updated and eventually updated every hour by the GitHub Action. So it needs to know where the list begins, and where it ends. The content inbetween will then be replaced with the latest content. As an example it could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Hey there 👋&lt;/span&gt;
I'm Simon and live in Berlin as a Freelance Frontend developer.

&lt;span class="gh"&gt;# Last 5 Books I've Read 🤓&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- GOODREADS-LIST:START --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- GOODREADS-LIST:END --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can customize the &lt;code&gt;GOODREADS-LIST&lt;/code&gt; tags, so you can have multiple workflows updating different sections in your README. This is also what I'm using to get to the screenshot at the top of this post.&lt;/p&gt;

&lt;h3&gt;
  
  
  #2 Create a workflow file
&lt;/h3&gt;

&lt;p&gt;Create a file in your own GitHub profile repo so it's located at: &lt;code&gt;{REPO}/.github/workflows/goodreads-books-workflow.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub Actions look for &lt;code&gt;*.yml&lt;/code&gt; files in the &lt;code&gt;/.github/workflows&lt;/code&gt; directory. These files are called workflows.&lt;/p&gt;

&lt;p&gt;In this workflow file you can paste the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Latest book list from goodreads&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Run workflow automatically&lt;/span&gt;
    &lt;span class="c1"&gt;# This will make it run every hour&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="c1"&gt;# Run workflow manually (without waiting for the cron to be called), through the Github Actions Workflow page directly&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update-readme-with-goodreads-books&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update this repo's README with what you're currently reading&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zwacky/goodreads-profile-workflow@main&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Replace this with your goodreads user id (go to "My Books" on goodreads to see it in the URL)&lt;/span&gt;
          &lt;span class="na"&gt;goodreads_user_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;92930971"&lt;/span&gt;
          &lt;span class="na"&gt;shelf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currently-reading"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last lines are input parameters. Make sure you change them with your Goodreads ID.&lt;/p&gt;

&lt;p&gt;Have a look at all the &lt;a href="https://github.com/zwacky/goodreads-profile-workflow#inputs" rel="noopener noreferrer"&gt;input parameters and template variables&lt;/a&gt; that goodreads-profile-workflow support.&lt;/p&gt;

&lt;h3&gt;
  
  
  #3 Commit and trigger the workflow
&lt;/h3&gt;

&lt;p&gt;If you head over to the Actions tab in the repo. You should see your "Latest book list from goodreads" workflow. This is also where you can see if the job ran successfully or if there is an issue or typo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwicki.io%2Fposts%2F2021-04-goodreads-workflow-for-github-actions%2Frun-github-actions-manually.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwicki.io%2Fposts%2F2021-04-goodreads-workflow-for-github-actions%2Frun-github-actions-manually.png" alt="manually trigger workflow for GitHub Actions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alternatively you could also wait until the scheduler picks up on the &lt;code&gt;"0 * * * *"&lt;/code&gt; setting (every hour) in the yml workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  #4 Done 🎉
&lt;/h3&gt;

&lt;p&gt;Hope you find this workflow useful! Please reach out via &lt;a href="https://github.com/zwacky/goodreads-profile-workflow/issues" rel="noopener noreferrer"&gt;GitHub issues&lt;/a&gt; or &lt;a href="https://twitter.com/zwacky" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;. Excited to hear from all the different reading lists and how your profile looks. Please consider leaving a GitHub star.&lt;/p&gt;




&lt;p&gt;Simon Wicki is a Freelance Developer in Berlin. Worked on Web and Mobile apps at JustWatch. Fluent in Vue, Angular, React and Ionic. Passionate about Frontend, tech, web perf &amp;amp; non-fiction books.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://twitter.com/zwacky" rel="noopener noreferrer"&gt;Join me on Twitter to follow my latest updates.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>portfolio</category>
    </item>
  </channel>
</rss>
