<?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>DEV Community: Guilherme Oenning</title>
    <description>The latest articles on DEV Community by Guilherme Oenning (@goenning).</description>
    <link>https://dev.to/goenning</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%2F116126%2F885e217e-89d3-4148-9e6e-c830898452cf.jpg</url>
      <title>DEV Community: Guilherme Oenning</title>
      <link>https://dev.to/goenning</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/goenning"/>
    <language>en</language>
    <item>
      <title>🔥 Why I chose Tauri instead of Electron 🔥</title>
      <dc:creator>Guilherme Oenning</dc:creator>
      <pubDate>Mon, 31 Jul 2023 12:33:44 +0000</pubDate>
      <link>https://dev.to/goenning/why-i-chose-tauri-instead-of-electron-34h9</link>
      <guid>https://dev.to/goenning/why-i-chose-tauri-instead-of-electron-34h9</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I struggled to get started with Electron, which led me to find Tauri. It got me hooked due to it's incredible developer experience and I thought I'd share the journey of building an app with Tauri.&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%2F1debds2drneuy5t6413g.gif" 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%2F1debds2drneuy5t6413g.gif"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it all started...
&lt;/h2&gt;

&lt;p&gt;About a year ago, I decided to experiment with building a Desktop application.&lt;/p&gt;

&lt;p&gt;I wasn't happy with the other apps in the niche I was working on, and I thought I could build something better. I have worked as a full-stack developer for a very long time, but I had never built a desktop application before.&lt;/p&gt;

&lt;p&gt;My first thought was to build with SwiftUI. Developers love native apps, and I've always wanted to learn Swift. However, building on SwiftUI would limit my audience to macOS users only. I had a feeling that most of the users would be on macOS anyway, but why limit myself when I could build a cross-platform app?&lt;/p&gt;

&lt;p&gt;Looking back now, I'm really glad I discarded SwiftUI early on. Just look at the diverse number of operating systems that people are using my app on.&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%2Frcnkg8ovtc2f14pwfjaj.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%2Frcnkg8ovtc2f14pwfjaj.png" alt="ImageOperating Systems Analytics for Aptakube showing a large amount of Windows and Linux users"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the way, this screenshot is from Aptabase, an open source analytics platform I'm working on. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aptabase/aptabase" rel="noopener noreferrer"&gt;https://github.com/aptabase/aptabase&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drop us a ⭐️ if you like it!&lt;/p&gt;

&lt;h2&gt;
  
  
  What about Electron?
&lt;/h2&gt;

&lt;p&gt;I don't live under a rock, so I knew that &lt;a href="https://www.electronjs.org/" rel="noopener noreferrer"&gt;Electron&lt;/a&gt; was a thing and that many of the popular apps I use daily are built on Electron, including the editor I'm using right now to write this post. It seemed like a perfect fit for what I was trying to do because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Single code base can target multiple platforms.&lt;/li&gt;
&lt;li&gt;✅ Works with React + TypeScript + Tailwind, which I'm already familiar with.&lt;/li&gt;
&lt;li&gt;✅ Very popular = lots of resources and guides.&lt;/li&gt;
&lt;li&gt;✅ NPM is the largest (is it?) package ecosystem out there, which means I can ship things faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other benefit of building on Electron was that I'd be able to focus on building the app rather than learning something completely new. I love learning new languages and frameworks, but I wanted to build something useful quickly. I'd still have to learn about Electron itself, but it wouldn't be as steep as learning Swift and SwiftUI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Okay, let's get started!
&lt;/h2&gt;

&lt;p&gt;I was settled. &lt;a href="https://aptakube.com" rel="noopener noreferrer"&gt;Aptakube&lt;/a&gt; was going to be built with Electron.&lt;/p&gt;

&lt;p&gt;I normally don't read the documentation. I know I should, but I don't. However, I always read the &lt;code&gt;Getting Started&lt;/code&gt; section whenever I pick a framework for the first time.&lt;/p&gt;

&lt;p&gt;Popular frameworks have a &lt;code&gt;npx create {framework-name}&lt;/code&gt; that quickly bootstraps an app for us. Next.js, Expo, Remix, and a lot of others have this. I found this super useful because they allow you to get started quickly, and they often give you a bunch of options like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do you want TypeScript or JavaScript?&lt;/li&gt;
&lt;li&gt;Do you want to use a CSS framework? What about Tailwind?&lt;/li&gt;
&lt;li&gt;Prettier and/or ESLint?&lt;/li&gt;
&lt;li&gt;Do you want this or that?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The list goes on. It's such a great developer experience that I wish every framework had one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I just &lt;code&gt;npx create electron-app&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Apparently, I can't, or at least I haven't found a way to do it, certainly not in the &lt;code&gt;Getting Started&lt;/code&gt; section.&lt;/p&gt;

&lt;p&gt;What I found instead was a &lt;code&gt;quick-start&lt;/code&gt; template that I could clone from Git, install the dependencies, and be on my way.&lt;/p&gt;

&lt;p&gt;However, it's not TypeScript, there is no bundler, no CSS framework, no linting, no formatting, nothing. It's just a bare-bones app that opens a window.&lt;/p&gt;

&lt;p&gt;I started building with this template and adding all the things I wanted to make it work. I thought it would be easy, but it wasn't.&lt;/p&gt;

&lt;p&gt;An electron app has three entry points: &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;preload&lt;/code&gt;, and &lt;code&gt;renderer&lt;/code&gt;. Wiring that all up with Vite was painful. I spent around 2 weeks of my free time trying to get everything working. I failed, and I got frustrated.&lt;/p&gt;

&lt;p&gt;I then found dozens of other boilerplates for Electron. I tried about five of them. Some were OK'ish, but I was a bit put off by the fact that most templates were too opinionated and installed way too many dependencies that I didn't even know what they were used for. Some didn't even work at all as they've been abandoned for years.&lt;/p&gt;

&lt;p&gt;In summary, the developer experience for someone new to Electron is below average. Next.js and Expo have set the bar so high that I've come to expect that every framework would offer a similar experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what now?
&lt;/h2&gt;

&lt;p&gt;While mindlessly scrolling through Twitter, I saw a tweet from &lt;a href="https://twitter.com/TauriApps" rel="noopener noreferrer"&gt;Tauri&lt;/a&gt; about the 1.0 release. They had been around for 2 years at that point, but I had no idea what Tauri was. I went to their website, and I was blown away 🤯 It seemed like exactly what I was looking for.&lt;/p&gt;

&lt;p&gt;Do you know the best part? They had a &lt;code&gt;npm create tauri-app&lt;/code&gt; instruction right there on the home page.&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%2Fmgeptrkv2snp7fo9v39m.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%2Fmgeptrkv2snp7fo9v39m.png" alt="Tauri nailed the developer experience from day 1 with the npx create tauri-app command"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I decided to give it a try. I ran the create the tauri-app command, and the experience was very similar to Next.js. It asked me a few questions, and then it created a new project for me based on my choices.&lt;/p&gt;

&lt;p&gt;At the end of the process I could simply run &lt;code&gt;npm run dev&lt;/code&gt; and I had a working app with hot-reload, TypeScript, Vite and Solid.js, pretty much everything I needed to get started. I was impressed and excited to learn more. I had to add Prettier, Linters, Tailwind and all that kind of stuff, but I'm used to it, and it was easier than in Electron.&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%2Fjp6u4631jgy2vx476494.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%2Fjp6u4631jgy2vx476494.png" alt="The experience you get when creating a new Tauri app"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started (again 😅), but with Tauri
&lt;/h2&gt;

&lt;p&gt;While in Electron I could build the whole app with just JavaScript/HTML/CSS, in Tauri the backend is in Rust and only the frontend is JavaScript. That obviously meant I had to learn Rust, which I was excited about, but also did not want to spend much time on as I wanted to build the prototype quickly.&lt;/p&gt;

&lt;p&gt;I've used 7+ programming languages professionally, so I thought learning Rust would be a walk in the park.&lt;/p&gt;

&lt;p&gt;I was wrong. I was so wrong. Rust is hard, like really hard, or at least it was for me!&lt;/p&gt;

&lt;p&gt;One year later, after more than 20 releases of my app, I still can't say I truly know Rust. I know enough to be constantly shipping new features on a regular basis, but I'm still learning so much every time I have to write something in Rust. GitHub Copilot and ChatGPT have been a huge help, and I still use them both a lot.&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%2Ffk2bcch6837t54rv1k0z.jpg" 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%2Ffk2bcch6837t54rv1k0z.jpg" alt="Strings in Rust is a lot more complicated than in other languages"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, there is something in Tauri that makes this process a lot easier.&lt;/p&gt;

&lt;p&gt;Tauri has the concept of a &lt;code&gt;Command&lt;/code&gt;, which is like a bridge between the frontend and backend. You can define commands in your Tauri backend using Rust and call them from JavaScript. Tauri itself provides a bunch of commands that you can use out of the box. For example, you can open a file dialog, read/update/delete a file, make HTTP requests, and a lot of other interactions with the operating system right from JavaScript, without having to write any Rust code.&lt;/p&gt;

&lt;p&gt;Well, what if you need to do something that is not available in Tauri? That's where &lt;code&gt;Plugins&lt;/code&gt; come in. Plugins are Rust libraries that define commands you can use in your Tauri app. I'll talk more about plugins later but just think of them as a way to extend Tauri's functionality.&lt;/p&gt;

&lt;p&gt;I've actually asked a lot of people building apps in Tauri if they had to write Rust code to build their apps. Most of them said they had to write very little Rust for some niche use cases. It's totally possible to build a Tauri app without writing any Rust code at all!&lt;/p&gt;

&lt;h2&gt;
  
  
  So how does Tauri compare to Electron?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Programming Language and Ecosystem
&lt;/h3&gt;

&lt;p&gt;In Electron, your backend is a Node.js process and your frontend is Chromium, which means a web developer can build a desktop app with just JavaScript/HTML/CSS. There's a huge ecosystem of libraries on NPM and there is just so much content about it on the internet that makes the learning process a lot easier.&lt;/p&gt;

&lt;p&gt;However, while it's generally seen as a good thing to be able to share code between the backend and frontend, it can also get confusing as developers might try to use backend functions on the frontend and vice versa. So you'd have to be careful not to mix things up.&lt;/p&gt;

&lt;p&gt;In contrast, Tauri's backend is Rust, and the frontend is also a webview (more on this next). While there are a significant number of Rust libraries, they're nowhere near the size of NPM. The Rust community is also a lot smaller than the JavaScript community, which means there's less content about it on the internet. But as mentioned above, depending on what you'll be building, you might not even need to write much Rust code at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My opinion:&lt;/strong&gt; I simply love the clear separation of backend and frontend that we get in Tauri. If I'm writing a piece of code in Rust, I know it'll be running as an OS process, and I have access to the network, file system, and a bunch of other things, while everything I have written in JavaScript is guaranteed to be running on a webview. Learning Rust has not been easy for me, but I'm enjoying the process, and I'm learning a lot of new things in general! Rust has started to grow in me 😊&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Webview
&lt;/h3&gt;

&lt;p&gt;In Electron, the frontend is a Chromium webview that is bundled with the app. This means you can always be certain of the version of Node.js and Chromium version used by your app, regardless of the operating system. This comes with major benefits, but also some drawbacks.&lt;/p&gt;

&lt;p&gt;The biggest benefit is the ease of development and testing, you know what features are available, and if something works on macOS, it'll more than likely work on Windows and Linux as well. The drawback, however, is that your app size will be a lot bigger because of all these binaries bundled into it.&lt;/p&gt;

&lt;p&gt;Tauri takes a drastically different approach. Instead of bundling Chromium with your app, it uses the operating system's default webview. This means that on macOS your app will use WebKit (Safari's engine), on Windows it'll use WebView2 (which is based on Chromium) and on Linux it'll use WebKitGTK (same as Safari's).&lt;/p&gt;

&lt;p&gt;The end result is an extremely small app that feels super fast!&lt;/p&gt;

&lt;p&gt;As a reference, my Tauri app weighs 24.7MB on macOS, while my competitor's app (Electron) weighs 1.3GB. 😱&lt;/p&gt;

&lt;p&gt;Why does it matter?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's so much faster to download and install.&lt;/li&gt;
&lt;li&gt;It costs less to host and distribute (I run on AWS so I pay for bandwidth and storage).&lt;/li&gt;
&lt;li&gt;I often get asked if my app is built with Swift, as users usually have a "this feels like a native app" moment when they see such a small and fast app.&lt;/li&gt;
&lt;li&gt;Security is handled by the OS. If there's a security issue with WebKit, Apple will release a security update, and my app will simply use it. I don't have to ship an updated version of my app to fix it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;My opinion:&lt;/strong&gt; I like the fact that my app is so small and fast. I was initially worried about the lack of consistency between operating systems because that meant I needed to test my app on all 3 operating systems, but I haven't had any issues so far. Web developers are used to this anyway, as we have been building multi-browser apps for such a long time. Bundlers and polyfills also help a lot in this regard!&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Plugins
&lt;/h3&gt;

&lt;p&gt;I briefly mentioned this before, but I think it's worth going into more detail as it's one of the best features of Tauri, in my opinion. A plugin is a collection of Commands written in Rust that can be called from JavaScript. It allows developers to compose applications by putting together different plugins that can either be open source or defined within your app.&lt;/p&gt;

&lt;p&gt;It's a nice way of structuring an app, and it makes it easy to share code between different apps too!&lt;/p&gt;

&lt;p&gt;Some examples of plugins you'll find in the Tauri ecosystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/tauri-apps/tauri-plugin-log" rel="noopener noreferrer"&gt;tauri-plugin-log&lt;/a&gt; - Configurable logging.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tauri-apps/tauri-plugin-store" rel="noopener noreferrer"&gt;tauri-plugin-store&lt;/a&gt; - Persistent state for things like user settings&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tauri-apps/tauri-plugin-window-state" rel="noopener noreferrer"&gt;tauri-plugin-window-state&lt;/a&gt; - Persist window sizes and positions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tauri-apps/window-vibrancy" rel="noopener noreferrer"&gt;window-vibrancy&lt;/a&gt; - Make your windows vibrant.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tauri-apps/tauri-plugin-sql" rel="noopener noreferrer"&gt;tauri-plugin-sql&lt;/a&gt; - SQL client that works with SQLite and more&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/aptabase/tauri-plugin-aptabase" rel="noopener noreferrer"&gt;tauri-plugin-aptabase&lt;/a&gt; - Analytics for Tauri apps.&lt;/li&gt;
&lt;li&gt;and a &lt;a href="https://github.com/tauri-apps/awesome-tauri#plugins" rel="noopener noreferrer"&gt;lot more&lt;/a&gt;...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These features &lt;strong&gt;could&lt;/strong&gt; have been part of Tauri itself, but having them separately means you can pick and choose what you want to use. It also means they can evolve independently and be replaced with alternatives if something better gets released.&lt;/p&gt;

&lt;p&gt;The plugin system was the second biggest reason why I chose Tauri; it makes the developer experience 1000x better!&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Feature Parity
&lt;/h3&gt;

&lt;p&gt;When it comes to features, both Electron and Tauri are very similar. Electron still has a few more features, but Tauri is catching up fast. At least for my use case, Tauri has everything I need.&lt;/p&gt;

&lt;p&gt;The only major inconvenience for me was the lack of a &lt;code&gt;Native Context Menu&lt;/code&gt; API. This is a &lt;a href="https://github.com/tauri-apps/tauri/issues/4338" rel="noopener noreferrer"&gt;highly requested feature&lt;/a&gt; by the community and would make Tauri apps feel a lot more native. I'm currently doing this with JS/HTML/CSS which is OK but could be better. Hopefully, we'll see this land in Tauri 2 🤞&lt;/p&gt;

&lt;p&gt;But other than that, there's plenty in Tauri. Right out of the box you get notifications, tray, menu, dialog, file system, networking, window management, auto-updating, packaging, code signing, GitHub actions, sidecars, etc. And if you need something else, you can always write a plugin for it, or use one of the existing ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Mobile
&lt;/h3&gt;

&lt;p&gt;This one came as a surprise to me. At the time I'm writing this, Tauri has experimental support for iOS and Android. It seems like it was always part of the plan, but I didn't know about it when I started my app. I'm not sure if I'll ever use it, but it's nice to know it's there.&lt;/p&gt;

&lt;p&gt;This is something that is not possible with Electron, and probably never will be. So if you're planning to build a cross-platform mobile and desktop app, Tauri may be the way to go, as you'd likely be able to share a lot of code between them. Designing mobile-first interfaces with web technologies has gotten a lot easier over the years, so building a single interface that could run as a desktop and mobile app is not as crazy as it sounds.&lt;/p&gt;

&lt;p&gt;I'll just drop this here to get you all excited about the future of Tauri.&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%2F7lpyejifj08brznovjpu.jpg" 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%2F7lpyejifj08brznovjpu.jpg" alt="Hello World Tauri on watchOS"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As Jonas mentioned in &lt;a href="https://twitter.com/jonasKruckie/status/1668258236762292227" rel="noopener noreferrer"&gt;his tweet&lt;/a&gt;, this is experimental and hacky; it might take a long time to be production ready, but it's still very exciting to see the innovation happening in this space!&lt;/p&gt;

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

&lt;p&gt;I'm very happy with my decision to use Tauri. Combined with Solid.js I was able to make a really fast app, and people love it! I'm not saying it's always better than Electron, but if it has the features you need, I'd say give it a try! As mentioned before, you might not even need to write that much code in Rust, so don't get intimidated by that! You'll be surprised by how much you can do with just JavaScript.&lt;/p&gt;

&lt;p&gt;In case you're into Kubernetes, check out &lt;a href="https://aptakube.com" rel="noopener noreferrer"&gt;Aptakube&lt;/a&gt;, a Kubernetes Desktop Client built with Tauri 😊&lt;/p&gt;

&lt;p&gt;I'm now working on [Aptabase]&lt;a href="https://github.com/aptabase/aptabase" rel="noopener noreferrer"&gt;https://github.com/aptabase/aptabase&lt;/a&gt;), an open-source and privacy-friendly analytics platform for desktop and mobile apps. It already has SDKs for various frameworks, including Tauri and Electron. By the way, the Tauri SDK is packaged as a Tauri Plugin! 😄&lt;/p&gt;

&lt;p&gt;Lastly, I'm also active on &lt;a href="https://twitter.com/goenning" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;. Feel free to reach out if you have any questions or feedback. I love talking about Tauri!&lt;/p&gt;

&lt;p&gt;Thanks for reading! 👋&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>electron</category>
    </item>
    <item>
      <title>Complete Guide to Kubeconfig and Kubernetes Contexts</title>
      <dc:creator>Guilherme Oenning</dc:creator>
      <pubDate>Fri, 16 Jun 2023 09:04:18 +0000</pubDate>
      <link>https://dev.to/goenning/complete-guide-to-kubeconfig-and-kubernetes-contexts-5h1l</link>
      <guid>https://dev.to/goenning/complete-guide-to-kubeconfig-and-kubernetes-contexts-5h1l</guid>
      <description>&lt;h1&gt;
  
  
  What the heck is Kubeconfig anyway?
&lt;/h1&gt;

&lt;p&gt;When interacting with a SQL database such as Postgres, for example, developers often need the so called &lt;code&gt;Connection String&lt;/code&gt;, which is a string that contains all the information needed to connect to the database. This includes the database host, port, username, password, and so on. All this information is often stored in a single string, which is then used by the application to connect to the database.&lt;/p&gt;

&lt;p&gt;Kubeconfig is sort of a connection string, but for Kubernetes clusters. It contains all the information needed to connect to a Kubernetes cluster, including the cluster host, port, authentication method, and so on.&lt;/p&gt;

&lt;p&gt;However, unlike the connection string, &lt;strong&gt;Kubeconfig is a file&lt;/strong&gt;. It's often stored in the &lt;code&gt;~/.kube/config&lt;/code&gt; file and it's used by pretty much all Kubernetes tools, such as &lt;a href="https://kubernetes.io/docs/tasks/tools/"&gt;kubectl&lt;/a&gt; and &lt;a href="https://dev.to/"&gt;Aptakube&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Let's take a look at a Kubeconfig file
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Config&lt;/span&gt;
&lt;span class="na"&gt;current-context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minikube&lt;/span&gt;
&lt;span class="na"&gt;clusters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;certificate-authority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/Users/goenning/.minikube/ca.crt&lt;/span&gt;
      &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://127.0.0.1:51171&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;minikube-local&lt;/span&gt;
&lt;span class="na"&gt;contexts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minikube-local&lt;/span&gt;
      &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&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;minikube&lt;/span&gt;
&lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&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;admin&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;client-certificate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/Users/goenning/.minikube/profiles/minikube/client.crt&lt;/span&gt;
      &lt;span class="na"&gt;client-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/Users/goenning/.minikube/profiles/minikube/client.key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a Kubeconfig file generated by &lt;a href="https://minikube.sigs.k8s.io/docs/"&gt;Minikube&lt;/a&gt;, which is a tool that creates a single-node Kubernetes cluster on your local machine. It's a great tool for learning Kubernetes and for developing applications locally.&lt;/p&gt;

&lt;p&gt;Each kubeconfig file contains three main sections: &lt;code&gt;clusters&lt;/code&gt;, &lt;code&gt;contexts&lt;/code&gt;, and &lt;code&gt;users&lt;/code&gt;. These sections are defined as an array of objects because you can have multiple clusters, contexts, and users in a single kubeconfig file. However, when working with multiple clusters it's often recommended to keep a separate kubeconfig file for each cluster, or at least group some clusters together in a single kubeconfig file based on whatever they have in common.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; is what links the &lt;code&gt;cluster&lt;/code&gt; and the &lt;code&gt;user&lt;/code&gt; together. Every operation you perform in Kubernetes is done in a context, which is why &lt;code&gt;kubectl&lt;/code&gt; has a &lt;code&gt;--context&lt;/code&gt; parameter for you to specific which cluster you want to interact with.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;cluster&lt;/code&gt; object defines where the cluster is located (server) and what client certificate (certificate-authority) to use during the SSL handshake. This section may also contain other settings such as &lt;code&gt;proxy-url&lt;/code&gt; in cases where the cluster can only be accessed through a proxy.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;user&lt;/code&gt; object defines the authentication method to be used when connecting to the cluster. In this case, the user is using a client certificate, which is a common authentication method for local clusters.&lt;/p&gt;

&lt;h1&gt;
  
  
  Using the Kubeconfig file with kubectl
&lt;/h1&gt;

&lt;p&gt;When using kubectl (or any other Kubernetes tool), by default it will look for the kubeconfig file in the &lt;code&gt;~/.kube/config&lt;/code&gt; path. However, you can specify a different path using the &lt;code&gt;--kubeconfig&lt;/code&gt; parameter or the &lt;code&gt;KUBECONFIG&lt;/code&gt; environment variable. Here are some examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="c"&gt;# uses ~/.kube/config&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt; /path/to/kubeconfig &lt;span class="c"&gt;# uses config from /path/to/kubeconfig&lt;/span&gt;
&lt;span class="nv"&gt;KUBECONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/kubeconfig kubectl get pods &lt;span class="c"&gt;# same as above&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In case you have multiple clusters in your kubeconfig file, you can use the &lt;code&gt;--context&lt;/code&gt; parameter to specify which cluster you want to interact with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;--context&lt;/span&gt; prod-europe &lt;span class="c"&gt;# uses the prod-europe context from the ~/.kube/config&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;--context&lt;/span&gt; prod-europe &lt;span class="nt"&gt;--kubeconfig&lt;/span&gt; /path/to/kubeconfig &lt;span class="c"&gt;# uses the prod-europe context from /path/to/kubeconfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting the &lt;code&gt;--kubeconfig&lt;/code&gt; parameter on every command is tedious, so you can also set the &lt;code&gt;KUBECONFIG&lt;/code&gt; environment variable to point to the kubeconfig file you want to use. This is especially useful when you have multiple kubeconfig files that you interact with constantly.&lt;/p&gt;

&lt;p&gt;If you're on macOS or Linux, you can set the KUBECONFIG on your shell profile (e.g. &lt;code&gt;~/.bash_profile&lt;/code&gt; or &lt;code&gt;~/.zshrc&lt;/code&gt;) so that it's always available on future shell session and you don't have to set it manually every time.&lt;/p&gt;

&lt;p&gt;Here's a cheat sheet with some useful context related commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl config get-contexts             &lt;span class="c"&gt;# display list of contexts&lt;/span&gt;
kubectl config current-context          &lt;span class="c"&gt;# display the current-context&lt;/span&gt;
kubectl config use-context prod-europe  &lt;span class="c"&gt;# set the default context to my-cluster-name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kubectl config use-context&lt;/code&gt; basically modifies your Kubeconfig file and sets the &lt;code&gt;current-context&lt;/code&gt; to the one you specified. This is useful when you want to switch between contexts without having to specify the &lt;code&gt;--context&lt;/code&gt; parameter every time.&lt;/p&gt;

&lt;h1&gt;
  
  
  Authentication and Security
&lt;/h1&gt;

&lt;p&gt;Last topic we'd like to cover is related to how to keep your Kubeconfig file secure. The Kubeconfig file may contain sensitive information such as tokens and private keys, so it's important to keep it safe. However, the the best option to protect your cluster is by &lt;strong&gt;not having any sensitive information&lt;/strong&gt; in your Kubeconfig file.&lt;/p&gt;

&lt;p&gt;Authentication is where things can get tricky. There are many ways to authenticate to a Kubernetes cluster, and some of them are more secure than others. Some popular ones are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token:&lt;/strong&gt; This is by far the worst authentication method in terms of security. If you Kubeconfig gets leaked, unless you've got some other network protection, anyone can use the token to access your cluster. Avoid using this for any important cluster, but it's generally OK to use on local clusters for testing/development purposes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Certificate:&lt;/strong&gt; This is somewhat similar to a token, however it can slightly safer if the content of the certificate is usually stored in a separate file. So even if the Kubeconfig gets leaker, the attacked might not have got access to the certificates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exec Plugins:&lt;/strong&gt; This is what most cloud providers and managed Kubernetes services would recommend you to use. It's essentially an extension of Kubeconfig to use external CLI tools (such as the aws CLI, az CLI or gcloud CLI) to perform the authentication using a cloud-based IAM mechanism. This is the most secure authentication method because it doesn't require any sensitive information to be stored in the Kubeconfig file. However, it's also the most complex one to setup as it requires additional knowledge of each cloud provider and how to use their CLI tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Connecting to multiple clusters simultaneously
&lt;/h1&gt;

&lt;p&gt;When working with multiple Kubernetes clusters, it might be useful to connect to more than one cluster at the same time so you can see the status of your application across multiple clusters in one view. This is especially useful when you're running an application across different regions or even multi-cloud.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aptakube.com"&gt;Aptakube&lt;/a&gt; is GUI application for Kubernetes that uses the same Kubeconfig files we mentioned above and can connect to multiple clusters at the same time. It essentially presents all your Kubernetes resources as if it was a single clusters. We invite you to try it out free today 😊&lt;/p&gt;

</description>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How to detect iPadOS with Swift</title>
      <dc:creator>Guilherme Oenning</dc:creator>
      <pubDate>Tue, 04 Apr 2023 11:37:20 +0000</pubDate>
      <link>https://dev.to/goenning/how-to-detect-ipados-with-swift-2n6i</link>
      <guid>https://dev.to/goenning/how-to-detect-ipados-with-swift-2n6i</guid>
      <description>&lt;p&gt;The iPadOS was first introduced in 2019 and it is an operating system based on iOS. In some cases you might want to add some extra functionality to your app when running on iPadOS, or maybe you want to show a different UI. This short post will show you how to detect iPadOS with Swift.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;UIUserInterfaceIdiom&lt;/code&gt; enum is what you want to use to detect the user interface idiom, which can be accessed via the &lt;code&gt;userInterfaceIdiom&lt;/code&gt; property on the &lt;code&gt;UIDevice&lt;/code&gt; class. The &lt;code&gt;UIUserInterfaceIdiom&lt;/code&gt; enum has a &lt;code&gt;pad&lt;/code&gt; option which you can use to detect if the device is an iPad.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kt"&gt;UIDevice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userInterfaceIdiom&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pad&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"This is an iPad!"&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;But wait, there is more! Just because it's an iPad doesn't mean it's running iPadOS. Its only since the iOS 13 release that the iPad has been able to run iPadOS. So if you want to detect iPadOS specifically you can use the &lt;code&gt;systemVersion&lt;/code&gt; property on the &lt;code&gt;UIDevice&lt;/code&gt; class to check if the version is greater than or equal to 13.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;#available(iOS 13.0, *)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kt"&gt;UIDevice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userInterfaceIdiom&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pad&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"This is an iPad, and it's running iPadOS!"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What about the &lt;code&gt;UIDevice.current.systemName&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;This property returns the name of the operating system. As of 2023, this property correctly returns &lt;code&gt;iPadOS&lt;/code&gt; for iPad devices running iPadOS. But this was not always the case, up until iPadOS 15 this property would return &lt;code&gt;iOS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you're targeting iOS 15 or later you can use this property to detect iPadOS. But if you're targeting iOS 14 or earlier you should use the method described above.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>ipados</category>
    </item>
    <item>
      <title>How to switch to Azure kubelogin</title>
      <dc:creator>Guilherme Oenning</dc:creator>
      <pubDate>Wed, 21 Dec 2022 17:05:57 +0000</pubDate>
      <link>https://dev.to/goenning/how-to-switch-to-azure-kubelogin-4ld</link>
      <guid>https://dev.to/goenning/how-to-switch-to-azure-kubelogin-4ld</guid>
      <description>&lt;p&gt;If you're using Azure Kubernetes Service (AKS) you may have already seen this warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ~ kubectl get pods
WARNING: the azure auth plugin is deprecated &lt;span class="k"&gt;in &lt;/span&gt;v1.22+, unavailable &lt;span class="k"&gt;in &lt;/span&gt;v1.26+&lt;span class="p"&gt;;&lt;/span&gt; use https://github.com/Azure/kubelogin instead.
To learn more, consult https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or even this one if you're running kubectl v1.26+&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ ~ kubectl get pods
error: The azure auth plugin has been removed.
Please use the https://github.com/Azure/kubelogin kubectl/client-go credential plugin instead.
See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins &lt;span class="k"&gt;for &lt;/span&gt;further details
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's pretty self-explanatory, the azure auth provider was removed after being deprecated for many months, and Azure/kubelogin is the future.&lt;/p&gt;

&lt;p&gt;But then you visit &lt;a href="https://github.com/Azure/kubelogin" rel="noopener noreferrer"&gt;https://github.com/Azure/kubelogin&lt;/a&gt; expecting an easy to follow guide on how it works and how to switch to kubelogin, but all you see is a wall of text and lots, lots of commands. I don't know about you, but it took me a while to figure out how that works. So I decided to write this post to help you get started. 😊&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the basics: What is the Azure Auth Provider?
&lt;/h2&gt;

&lt;p&gt;You can skip this if you want, but it might be good for you to understand how the authentication to AKS works, I'll keep it brief and to the point.&lt;/p&gt;

&lt;p&gt;When connecting to AKS for the first time, you most likely executed this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks get-credentials &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;your-resource-group&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;your-aks-name&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command generates a few sections on your &lt;code&gt;~/.kube/config&lt;/code&gt; file, which is used by kubectl to connect to the cluster.&lt;/p&gt;

&lt;p&gt;The relevant section looks like this:&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;users&lt;/span&gt;&lt;span class="pi"&gt;:&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;clusterUser_{your-resource-group}_{your-aks-name}&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;auth-provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;apiserver-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;...&lt;/span&gt;
          &lt;span class="na"&gt;config-mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1'&lt;/span&gt;
          &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzurePublicCloud&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&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;azure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This config is using the aforementioned azure auth provider, which is now deprecated. The logic used by the auth provider is embedded in the kubectl binary, which is why you can authenticate and connect to the cluster without having to install anything else.&lt;/p&gt;

&lt;p&gt;Starting from Kubernetes v1.26 (December/2022), this auth provider will be removed from the kubectl binary, so you'll need an alternative method to be able to authenticate to AKS.&lt;/p&gt;

&lt;p&gt;If the deprecation wasn't enough reason to switch, can I say that the current developer experience with azure auth provider is very poor as well?&lt;/p&gt;

&lt;p&gt;During the authentication phase, you're presented with a UR that gives you a "Device Code", which you then enter into the terminal. This is how the auth provider knows that you're the one trying to connect to the cluster. This process is slow because it's interactive, you need to switch windows, click on buttons and then copy/paste some text.&lt;/p&gt;

&lt;p&gt;If you don't like it either, you're in luck, because kubelogin is a thousand times better 🤩.&lt;/p&gt;

&lt;h2&gt;
  
  
  Downloading kubelogin
&lt;/h2&gt;

&lt;p&gt;The authentication plugins are being moved out of the &lt;code&gt;kubectl&lt;/code&gt; binary into separate binaries, maintained by the Cloud providers and distributed independently.&lt;/p&gt;

&lt;p&gt;This is why the first step is to download the kubelogin binary. Is this specific to Azure? No, the GCP Auth Provider is also being replaced by the gcloud CLI.&lt;/p&gt;

&lt;p&gt;To install kubelogin, follow the &lt;a href="https://github.com/Azure/kubelogin#setup" rel="noopener noreferrer"&gt;official guide&lt;/a&gt; and come back here when you're done 😁.&lt;/p&gt;

&lt;p&gt;The binary should be on your &lt;code&gt;PATH&lt;/code&gt;. You can verify that kubelogin is installed correctly by running &lt;code&gt;which kubelogin&lt;/code&gt; on macOS/Linux or &lt;code&gt;where kubelogin&lt;/code&gt; on Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ready to convert?
&lt;/h2&gt;

&lt;p&gt;So here's where things get a bit more complicated and they don't really explain it very well.&lt;/p&gt;

&lt;p&gt;To connect to AKS you need a token. The token is generated by the Azure Active Directory (AAD) and is used to make API calls to the Kubernetes API Server. This is what the azure auth provider does for you, it generates the token for you transparently.&lt;/p&gt;

&lt;p&gt;There are many (many!) ways to generate a token in Azure and it's beyond the point of this article to cover them all, but you might have heard or used some of them, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service Principal&lt;/li&gt;
&lt;li&gt;User Principal Principal&lt;/li&gt;
&lt;li&gt;Managed Service Identity&lt;/li&gt;
&lt;li&gt;Azure CLI&lt;/li&gt;
&lt;li&gt;Azure Workload Federated Identity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;kubelogin convert-kubeconfig&lt;/code&gt; is the command we'll be using next. It'll modify our kubeconfig and replace the existing azure auth provider with a different authentication method.&lt;/p&gt;

&lt;p&gt;Which one? It's your choice.&lt;/p&gt;

&lt;p&gt;As a developer, if you're connecting to AKS from your machine, you're most likely to use the &lt;code&gt;az&lt;/code&gt; Azure CLI. In fact, you probably already have it installed and configured, because that's what the first command on this article uses.&lt;/p&gt;

&lt;p&gt;The Azure CLI can be used to generate tokens using the user's identity, which is great for development. Permissions are managed through Azure RBAC so administrators can control what developers can do on the cluster.&lt;/p&gt;

&lt;p&gt;If you choose to use Azure CLI, stick with me. But if prefer another option, find the relevant section on &lt;a href="https://github.com/Azure/kubelogin#setup" rel="noopener noreferrer"&gt;official guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To convert your kubectl authentication method to Azure CLI, all you need to do is run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubelogin convert-kubeconfig &lt;span class="nt"&gt;-l&lt;/span&gt; azurecli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shouldn't take more than a second to complete. If you inspect your &lt;code&gt;~/.kube/config&lt;/code&gt; again, you'll see that the azure auth provider has been replaced by kubelogin.&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;users&lt;/span&gt;&lt;span class="pi"&gt;:&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;clusterUser_{your-resource-group}_{your-aks-name}&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;client.authentication.k8s.io/v1beta1&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;get-token&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--login&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;azurecli&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--server-id&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;6dae42f8-4368-4678-94ff-3960e28e3630&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubelogin&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
        &lt;span class="na"&gt;provideClusterInfo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is using the new client-go credential plugin feature. The &lt;code&gt;kubelogin&lt;/code&gt; binary is now responsible for generating the token, which uses the Azure CLI behind the scenes.&lt;/p&gt;

&lt;p&gt;You can now use &lt;code&gt;kubectl&lt;/code&gt; as usual, it'll connect to the same clusters it has been before, but with a different authentication flow. You might need to run &lt;code&gt;az login&lt;/code&gt; once per day, but at least you won't be prompted for a Device Code anymore! 🎉&lt;/p&gt;

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

&lt;p&gt;It might feel a bit too magical when you're asked to simply run a CLI and things just work. But what did it actually do?&lt;/p&gt;

&lt;p&gt;I always try to understand what's going on behind the scenes so I troubleshoot it better when something goes wrong. I hope this article helped you understand what &lt;code&gt;convert-kubeconfig&lt;/code&gt; does. As you can see, there is no magic, it's all just text files and some code.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>azure</category>
      <category>aks</category>
    </item>
    <item>
      <title>Measuring the performance of a website from multiple locations on a budget</title>
      <dc:creator>Guilherme Oenning</dc:creator>
      <pubDate>Sun, 10 Feb 2019 19:38:11 +0000</pubDate>
      <link>https://dev.to/goenning/measuring-the-performance-of-a-website-from-multiple-locations-on-a-budget-1h52</link>
      <guid>https://dev.to/goenning/measuring-the-performance-of-a-website-from-multiple-locations-on-a-budget-1h52</guid>
      <description>&lt;h2&gt;
  
  
  Global metrics for websites with global visitors
&lt;/h2&gt;

&lt;p&gt;Measuring the performance of a website from multiple locations around the world is crucial with the current global scale of the internet. In most cases, your visitors are not only based in your home country but from all other countries too. From Canada to Australia, from Chile to Russia, your website is being visited by more people than you think.&lt;/p&gt;

&lt;p&gt;It's easy to forget this fact and simply measure the performance of a website from the local machine, where it's usually close to the website Data Center. Not only that, but this machine is probably a beast too, it can open multiple tabs of Google Chrome and load any page with an insane amount of JavaScript in half a second.&lt;/p&gt;

&lt;p&gt;But in reality, your visitors are using a 10 years old laptop with Windows 7 and are connected to the internet via a 3G network connection. Your Data Center is in San Francisco and these visitors are from The Philippines. Even your smartphone has 10x higher speed and lower latency than theirs.&lt;/p&gt;

&lt;p&gt;There are a number of paid services that allow you to constantly monitor your website performance from multiple locations around the world.&lt;/p&gt;

&lt;p&gt;But maybe you're a geek and you want to do it yourself?&lt;/p&gt;

&lt;p&gt;On this post, I want to show you how to collect website performance from multiple locations using open source tools on a low monthly cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  webpage-timing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/goenning/webpage-timing" rel="noopener noreferrer"&gt;goenning/webpage-timing&lt;/a&gt; is Node.js application that uses Headless Chrome to collect performance metrics from a web page.&lt;/p&gt;

&lt;p&gt;This application is also available as a Docker image, so you can run it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run goenning/webpage-timing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go on, give it a try and execute this on your Terminal.&lt;/p&gt;

&lt;p&gt;You should get a response similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2019-02-10T16:32:25.655Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1549816345655&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;430&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"iMac.local"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.org"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;300473.456095&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"Documents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"Frames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"JSEventListeners"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"Nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"LayoutCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"RecalcStyleCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"LayoutDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.053402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"RecalcStyleDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.000584&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"ScriptDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.000015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"TaskDuration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.068091&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"JSHeapUsedSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2108152&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"JSHeapTotalSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3936256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.org/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"entryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"navigation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"startTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;427.6149999932386&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"initiatorType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"navigation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"nextHopProtocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"h2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"workerStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"redirectStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"redirectEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"fetchStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2899999963119626&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"domainLookupStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;7.634999987203628&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"domainLookupEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.984999964013696&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"connectStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.984999964013696&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"connectEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;313.34999995306134&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"secureConnectionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"requestStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;313.6149999918416&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"responseStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;414.7049999446608&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"responseEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;415.8099999767728&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"transferSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"encodedBodySize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;606&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"decodedBodySize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1270&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"serverTiming"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"unloadEventStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"unloadEventEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"domInteractive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;426.6899999929592&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"domContentLoadedEventStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;426.7049999907613&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"domContentLoadedEventEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;426.7049999907613&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"domComplete"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;427.6049999753013&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"loadEventStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;427.6149999932386&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"loadEventEnd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;427.6149999932386&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"navigate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"redirectCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"first-paint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"entryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"startTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;491.65999999968335&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"first-contentful-paint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"entryType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"startTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;491.68499995721504&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
       &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;code&gt;Timing&lt;/code&gt; object, it contains some information collected from the Headless Chrome that executed inside the container. In the example above, it took &lt;strong&gt;430ms&lt;/strong&gt; to load &lt;strong&gt;&lt;a href="https://example.org" rel="noopener noreferrer"&gt;https://example.org&lt;/a&gt;&lt;/strong&gt;. If this page had any CSS/JavaScript/Image files, it'd also download it and the duration would have been higher.&lt;/p&gt;

&lt;p&gt;You can also specify some custom arguments, which allows you to collect metrics from any page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REQUEST_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://github.com/docker goenning/webpage-timing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll also notice that the &lt;code&gt;entries&lt;/code&gt; array has many more items now, which includes all the CSS/JavaScript/Image files the browser had to download.&lt;/p&gt;

&lt;p&gt;Another useful parameter is &lt;code&gt;MONGO_URL&lt;/code&gt;, which allows you to store the &lt;code&gt;Timing&lt;/code&gt; object into a MongoDB collection instead of printing it to stdout. It is useful when you want to keep a history of execution over time and perform further analysis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Replace the connection string below with your own&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MONGO_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mongodb://user:pass@your-server:port/db &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;REQUEST_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://github.com/docker goenning/webpage-timing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running it on a global infrastructure
&lt;/h2&gt;

&lt;p&gt;We've seen so far that we can collect metrics from a web page using a local Docker container and store it in MongoDB.&lt;/p&gt;

&lt;p&gt;To go one step further we'll use machines on the Cloud to run this container from multiple regions. You could spin up multiple Virtual Machines on the cloud provider of your preference and schedule this script on cron. But that's not very cost effective and you'd have to carry the burden of maintaining dozens of virtual machines.&lt;/p&gt;

&lt;p&gt;But there's a better way 😀&lt;/p&gt;

&lt;p&gt;Azure has a service called &lt;a href="https://azure.microsoft.com/en-us/services/container-instances/" rel="noopener noreferrer"&gt;Azure Container Instances&lt;/a&gt;. It allows you to run a Docker container on the cloud without having to worry about the infrastructure behind it. The best of all? You only pay per execution time. If you start a container that runs for 5 seconds, you'll pay $0.000080. On this post, I'll show you how to perform this operation on Azure, but If prefer AWS, search for &lt;code&gt;AWS Fargate&lt;/code&gt;, it's a similar service, so you apply the same idea presented here.&lt;/p&gt;

&lt;p&gt;What we're going to do is create dozens of Azure Container Instance on each of its 17 regions and configure it to execute &lt;code&gt;goenning/webpage-timing&lt;/code&gt; with our custom parameters. We'll also need to store the data from all locations to query it later. In this example, I'll be using &lt;a href="https://www.mongodb.com/cloud/atlas" rel="noopener noreferrer"&gt;MongoDB Atlas&lt;/a&gt; because it has a free tier and can also be hosted on Azure.&lt;/p&gt;

&lt;p&gt;Assuming that you have an Azure Account and the &lt;a href="https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" rel="noopener noreferrer"&gt;Azure CLI&lt;/a&gt; is installed, run the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;az login
&lt;span class="nv"&gt;$ &lt;/span&gt;az account &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--subscription&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;YOUR_SUBSCRIPTION NAME&amp;gt;"&lt;/span&gt; &lt;span class="c"&gt;# you can skip this if your account has only one subscription&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file &lt;code&gt;template.yaml&lt;/code&gt; with this content:&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2018-06-01&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Microsoft.ContainerInstance/containerGroups&lt;/span&gt;
&lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$location$&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;$name$&lt;/span&gt;
&lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&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;$name$-01&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;goenning/webpage-timing&lt;/span&gt;
      &lt;span class="na"&gt;environmentVariables&lt;/span&gt;&lt;span class="pi"&gt;:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ORIGIN'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$location$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REQUEST_URL'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$request_url$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MONGO_URL'&lt;/span&gt;
          &lt;span class="na"&gt;secureValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$mongo_url$'&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;
          &lt;span class="na"&gt;memoryInGb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7&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;$name$-02&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;goenning/webpage-timing&lt;/span&gt;
      &lt;span class="na"&gt;environmentVariables&lt;/span&gt;&lt;span class="pi"&gt;:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ORIGIN'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$location$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REQUEST_URL'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$request_url$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MONGO_URL'&lt;/span&gt;
          &lt;span class="na"&gt;secureValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$mongo_url$'&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;
          &lt;span class="na"&gt;memoryInGb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7&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;$name$-03&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;goenning/webpage-timing&lt;/span&gt;
      &lt;span class="na"&gt;environmentVariables&lt;/span&gt;&lt;span class="pi"&gt;:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ORIGIN'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$location$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REQUEST_URL'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$request_url$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MONGO_URL'&lt;/span&gt;
          &lt;span class="na"&gt;secureValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$mongo_url$'&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;
          &lt;span class="na"&gt;memoryInGb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7&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;$name$-04&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;goenning/webpage-timing&lt;/span&gt;
      &lt;span class="na"&gt;environmentVariables&lt;/span&gt;&lt;span class="pi"&gt;:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ORIGIN'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$location$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REQUEST_URL'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$request_url$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MONGO_URL'&lt;/span&gt;
          &lt;span class="na"&gt;secureValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$mongo_url$'&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;
          &lt;span class="na"&gt;memoryInGb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7&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;$name$-05&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;goenning/webpage-timing&lt;/span&gt;
      &lt;span class="na"&gt;environmentVariables&lt;/span&gt;&lt;span class="pi"&gt;:&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ORIGIN'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$location$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;REQUEST_URL'&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$request_url$'&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MONGO_URL'&lt;/span&gt;
          &lt;span class="na"&gt;secureValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$mongo_url$'&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;
          &lt;span class="na"&gt;memoryInGb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;
  &lt;span class="na"&gt;osType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Linux&lt;/span&gt;
  &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This template will be loaded into Azure Container Service and it'll create 5 containers using the same image and parameters. It's also been configured to use up to 0.4 of the CPU and only 700MB of Memory.&lt;/p&gt;

&lt;p&gt;Create another file &lt;code&gt;aci.sh&lt;/code&gt; with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The name of the resource group to be used on Azure&lt;/span&gt;
&lt;span class="nv"&gt;resource_group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"webtiming-rg"&lt;/span&gt;

&lt;span class="c"&gt;# The list of locations from where the test will be executed&lt;/span&gt;
&lt;span class="nv"&gt;locations&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt; &lt;span class="s2"&gt;"westus"&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt; &lt;span class="s2"&gt;"westeurope"&lt;/span&gt; &lt;span class="s2"&gt;"westus2"&lt;/span&gt; &lt;span class="s2"&gt;"northeurope"&lt;/span&gt; &lt;span class="s2"&gt;"southeastasia"&lt;/span&gt; &lt;span class="s2"&gt;"eastus2"&lt;/span&gt; &lt;span class="s2"&gt;"centralus"&lt;/span&gt; &lt;span class="s2"&gt;"australiaeast"&lt;/span&gt; &lt;span class="s2"&gt;"uksouth"&lt;/span&gt; &lt;span class="s2"&gt;"southcentralus"&lt;/span&gt; &lt;span class="s2"&gt;"centralindia"&lt;/span&gt; &lt;span class="s2"&gt;"southindia"&lt;/span&gt; &lt;span class="s2"&gt;"northcentralus"&lt;/span&gt; &lt;span class="s2"&gt;"eastasia"&lt;/span&gt; &lt;span class="s2"&gt;"canadacentral"&lt;/span&gt; &lt;span class="s2"&gt;"japaneast"&lt;/span&gt; &lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# The URL of the webpage we want to test&lt;/span&gt;
&lt;span class="nv"&gt;request_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/docker"&lt;/span&gt;

&lt;span class="c"&gt;# The connection string to a MongoDB instance&lt;/span&gt;
&lt;span class="nv"&gt;mongo_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"mongodb+srv://webtiming:webtiming@cluster0-s5g8l.azure.mongodb.net/webtiming?retryWrites=true"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"init"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;then
  &lt;/span&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$resource_group&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; eastus
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"run"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ./out
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; ./out
  &lt;span class="k"&gt;for &lt;/span&gt;loc &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ./template.yaml | 
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|\$name\$|'&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s1"&gt;'-wt|'&lt;/span&gt; | 
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|\$request_url\$|'&lt;/span&gt;&lt;span class="nv"&gt;$request_url&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; | 
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|\$location\$|'&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; | 
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|\$mongo_url\$|'&lt;/span&gt;&lt;span class="nv"&gt;$mongo_url&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; | 
    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s|\$location\$|'&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"./out/&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s2"&gt;-wt.yaml"&lt;/span&gt;

    &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az container show &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="nt"&gt;-wt&lt;/span&gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$resource_group&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; containers[0].instanceView.currentState.state 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;then
      &lt;/span&gt;az container start &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$resource_group&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s2"&gt;-wt"&lt;/span&gt; &amp;amp;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s2"&gt;-wt has started..."&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;az container create &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$resource_group&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$loc&lt;/span&gt; &lt;span class="nt"&gt;--file&lt;/span&gt; &lt;span class="s2"&gt;"./out/&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s2"&gt;-wt.yaml"&lt;/span&gt; &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$loc&lt;/span&gt;&lt;span class="s2"&gt;-wt has been created..."&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done
fi

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"clean"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;then
  &lt;/span&gt;az group delete &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$resource_group&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first lines are the parameters, configured it based on given comments and execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./aci.sh init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This first step will simply create a Resource Group based on the configured name. You can skip this step if you prefer to do it manually through Azure Portal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./aci.sh run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the most important part of the script. It'll basically loop through each configured location and create a YAML file based on &lt;code&gt;template.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By the end of the execution, you should have something similar to this on your Azure Portal.&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnuxlgkp14rcr0pohjys5.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fnuxlgkp14rcr0pohjys5.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that there are 17 container groups (1 per region) with 5 containers on each. Some of them have already finished processing, while others are still in progress. This process can take a few extra seconds as Azure needs to pull the images from Docker Hub Registry first.&lt;/p&gt;

&lt;p&gt;A few seconds later all groups should be on "Succeeded" state. By the end of this process, these instances will remain on Azure until you remove it. You can do so by executing &lt;code&gt;./aci.sh clean&lt;/code&gt;, which removes the Resource Group and all of its container instances.&lt;/p&gt;

&lt;p&gt;But if you plan to periodically execute this, you can keep the resources on Azure and simply execute &lt;code&gt;./aci.sh run&lt;/code&gt; again. The script is smart enough to restart the container if it already exists on Azure. You can repeat this process as many times as you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;We should now have a few documents on our MongoDB database, so we can now look at the data and perform some analysis. We could hook up any BI tool to this MongoDB instance, extract the data and plot some charts.&lt;/p&gt;

&lt;p&gt;But there is also &lt;a href="https://www.mongodb.com/products/charts" rel="noopener noreferrer"&gt;MongoDB Charts&lt;/a&gt;, which is a data visualization tool to create visual representations of our MongoDB Data. At the time of this writing, this service is on beta and free to use, so I decided to give it a try. This is what I got from my execution.&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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Ffjhtfqpbiw8c3ratdx82.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%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Ffjhtfqpbiw8c3ratdx82.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we can see, we have collected 595 timings and the global average is 4165ms. The chart shows that loading &lt;code&gt;https://github.com/docker&lt;/code&gt; on Asian is 2seconds slower when compared to US.&lt;/p&gt;

&lt;p&gt;You could go one step further and actually analyze the &lt;code&gt;entries&lt;/code&gt; array to find out which HTTP resources took longer to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here?
&lt;/h2&gt;

&lt;p&gt;If you liked this and want to take it to the next level, here are some ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Schedule this script to execute every X hours&lt;/li&gt;
&lt;li&gt;Reduce the Docker Image size. The smaller the image is, the faster it'll be executed on the Cloud, which means less 💸&lt;/li&gt;
&lt;li&gt;Fork the project and enhance it with extra timing information you need&lt;/li&gt;
&lt;li&gt;Use puppeteer to emulate a slower network and CPU&lt;/li&gt;
&lt;li&gt;Change the script to be a multi-step process. If you have an e-commerce and you want to measure how long does it take for a user to find a product and buy it. You can use this script as a starting point and include the extra steps of this process&lt;/li&gt;
&lt;li&gt;Go wild and deploy hundreds of containers per region 😁, just keep in mind that it'll also increase your 💸&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  That's all! 🎉
&lt;/h2&gt;

&lt;p&gt;What do you think about this? Please leave a comment if you any suggestion or feedback.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>web</category>
      <category>performance</category>
      <category>azure</category>
    </item>
    <item>
      <title>How we reduced our initial JS/CSS size by 67%</title>
      <dc:creator>Guilherme Oenning</dc:creator>
      <pubDate>Mon, 26 Nov 2018 16:52:39 +0000</pubDate>
      <link>https://dev.to/goenning/how-we-reduced-our-initial-jscss-size-by-67-3ac0</link>
      <guid>https://dev.to/goenning/how-we-reduced-our-initial-jscss-size-by-67-3ac0</guid>
      <description>&lt;p&gt;We have been working on reducing the amount of bytes that we send to all Fider users. Being a web application built with React, we have focused on JS and CSS. On this post we share our learnings, some concepts and suggestions on how you can do the same with your web application.&lt;/p&gt;

&lt;p&gt;Fider is built with React and Webpack on the frontend, so the topics below will be mostly useful for teams using same stack, but the concepts can also be applied to other stacks. It is also an open source, so you can actually see the Pull Requests and the source code: &lt;a href="https://github.com/getfider/fider" rel="noopener noreferrer"&gt;https://github.com/getfider/fider&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Webpack Bundle Analyzer&lt;/li&gt;
&lt;li&gt;Long term caching with content hash&lt;/li&gt;
&lt;li&gt;The common bundle&lt;/li&gt;
&lt;li&gt;Code Splitting on route level&lt;/li&gt;
&lt;li&gt;Loading external dependencies on demand&lt;/li&gt;
&lt;li&gt;Font Awesome and Tree Shaking&lt;/li&gt;
&lt;li&gt;Switching from big to small NPM packages&lt;/li&gt;
&lt;li&gt;Optimising the main bundle is crucial&lt;/li&gt;
&lt;li&gt;TSLib (TypeScript only)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Webpack Bundle Analyzer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/webpack-bundle-analyzer" rel="noopener noreferrer"&gt;webpack-bundle-analyzer&lt;/a&gt; is a webpack plugin that generates an interactive zoomable treemap of all your bundles. This has been crucial for us to understand which modules are inside each bundle. You can also see which are the biggest modules within each bundle.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you don’t know the root cause, how can you tackle it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is an example of what this plugin will generate for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.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%2Fvoqzfazpkylsmkq3uka2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2Fvoqzfazpkylsmkq3uka2.png" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Did you notice that huge &lt;strong&gt;entities.json&lt;/strong&gt; inside the vendor bundle? That's a good starting point to analyze the content of your bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Long term caching with content hash
&lt;/h2&gt;

&lt;p&gt;Long term caching is the process of telling the browser to cache a file for a long time, like 3 months or even 1 year. This is a important settings to ensure that returning users won’t need to download the same JS/CSS files over and over again.&lt;/p&gt;

&lt;p&gt;The browser will cache files based on its full path name, so if you need to force the user to download a new version of your bundle, you need to rename it. Luckly webpack provides a feature to generate the bundles with a dynamic name, hence forcing the browser to download new files only.&lt;/p&gt;

&lt;p&gt;We have previously used &lt;strong&gt;chunkhash&lt;/strong&gt; for a long time on our webpack configuration. 99% of the cases where you want long term cache, the best option is to use &lt;strong&gt;contenthash&lt;/strong&gt;, which will generate a hash based on its content.&lt;/p&gt;

&lt;p&gt;This technique does not reduce the bundle size, but it certainly helps to reduce the amount of times the user has to download our bundles. If the bundle didn’t change, don’t force the user to download it again.&lt;/p&gt;

&lt;p&gt;To learn more, visit the official documentation &lt;a href="https://webpack.js.org/guides/caching/" rel="noopener noreferrer"&gt;https://webpack.js.org/guides/caching/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The common bundle
&lt;/h2&gt;

&lt;p&gt;Combining all the NPM packages into a separate bundle has been a long time practice for many teams. This is very useful when combined with long term caching. &lt;/p&gt;

&lt;p&gt;NPM packages change less often than our app code, so we don’t need to force users to download all your NPM packages if nothing has changed. This is usually called the &lt;strong&gt;vendor bundle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But we can take this practice one step further. &lt;/p&gt;

&lt;p&gt;What about your own code that also change less often? Maybe you have a few basic components like Button, Grid, Toggle, etc. that have been created some time ago and haven't changed in a while.&lt;/p&gt;

&lt;p&gt;This is a good candidate for a &lt;strong&gt;common bundle&lt;/strong&gt;. You can check this &lt;a href="https://github.com/getfider/fider/pull/636" rel="noopener noreferrer"&gt;PR #636&lt;/a&gt; where we basically move all our own modules inside some specific folders into a common bundle. &lt;/p&gt;

&lt;p&gt;This will ensure that, unless we change our base components, the user won’t need to redownload it. &lt;/p&gt;

&lt;h2&gt;
  
  
  Code Splitting on route level
&lt;/h2&gt;

&lt;p&gt;Code splitting is currently a hot topic. This has been around for some time, but the tools and frameworks have evolved a lot, to the point where doing code splitting is much simpler now.&lt;/p&gt;

&lt;p&gt;It’s very common to have applications that push one big bundle that contains all the JS/CSS required to render any page within the application, even if the user is only looking at the Home page. We don’t know if the user will ever visit the Site Settings page, but we have pushed all the code for that already. Fider has been doing this for a long time and we now have changed it.&lt;/p&gt;

&lt;p&gt;The idea of Code Splitting is to generate multiple smaller bundles, usually one per route, and a main bundle. The only bundle we send to all the users is the main bundle, which will then asynchronously download all the required bundles to render the current page.&lt;/p&gt;

&lt;p&gt;It seems complicated, but thanks to React and Webpack, this is not rocket science anymore. For those using React &amp;lt;= 16.5, we recommend &lt;a href="https://github.com/jamiebuilds/react-loadable" rel="noopener noreferrer"&gt;react-loadable&lt;/a&gt;. If you’re already on React 16.6, then you can use React.lazy() which has been a new addition to this version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In this PR you can find how &lt;a href="https://github.com/cfilby" rel="noopener noreferrer"&gt;@cfilby&lt;/a&gt; (thank you!) added code splitting to Fider with react-loadable: &lt;a href="https://github.com/getfider/fider/pull/596" rel="noopener noreferrer"&gt;PR #596&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;After we migrated to React 16.6, we have then replaced this external package with React.lazy and Suspense: &lt;a href="https://github.com/getfider/fider/pull/646" rel="noopener noreferrer"&gt;PR #646&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also had issues with some rare events where users were having issues to download asynchronous bundles. A potential solution has been documented on &lt;a href="https://dev.to/goenning/how-to-retry-when-react-lazy-fails-mb5"&gt;How to retry when React lazy fails&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edit 4th Dec:&lt;/strong&gt; You might also consider using &lt;a href="https://github.com/smooth-code/loadable-components/" rel="noopener noreferrer"&gt;loadable&lt;/a&gt; as per &lt;a href="https://dev.to/thekashey/comment/7b04"&gt;Anton's comment&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading external dependencies on demand
&lt;/h2&gt;

&lt;p&gt;By using the Webpack Bundle Analyzer we noticed that our vendor bundle had all the content of react-toastify, which is the toaster library that we use. That is usually ok, except that 95% of the Fider users will never see a toaster message. There are very few places we show a toaster, so &lt;strong&gt;why do we push 30kB of JavaScript to every user if they don’t need it&lt;/strong&gt;? &lt;/p&gt;

&lt;p&gt;This is a similar problem to the one above, except that we are not talking about routes anymore, this is a feature used in multiple routes. Can you code split on a feature level?&lt;/p&gt;

&lt;p&gt;Yes, you can!&lt;/p&gt;

&lt;p&gt;In a nutshell, what you have to do is switch from static import to dynamic import.&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="c1"&gt;// before&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;toast&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="s2"&gt;./toastify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./toastify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello World&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Webpack will bundle the toastify module and all its NPM dependencies separately. The &lt;strong&gt;browser will then only download that bundle when the toast is needed&lt;/strong&gt;. If you have configured long term caching, then on the second toaster call it won’t have to download it again.&lt;/p&gt;

&lt;p&gt;The video below shows how it looks like on the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.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%2F91cc8ffano6fctza7k22.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.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%2F91cc8ffano6fctza7k22.gif" width="1024" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the details on how this was implemented on &lt;a href="https://github.com/getfider/fider/pull/645" rel="noopener noreferrer"&gt;PR #645&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Font Awesome and Tree Shaking
&lt;/h2&gt;

&lt;p&gt;Tree Shaking is the process of importing only what you need from a module and discarding the rest. This is enabled by default when running webpack on production mode.&lt;/p&gt;

&lt;p&gt;The usual approach to use Font Awesome is to import an external font file and a CSS that maps each character (icon) on that font to one CSS class. The result is that even though we only use icon A, B and C, we are forcing the browsers to download this external font and a CSS definition of 600+ icons.&lt;/p&gt;

&lt;p&gt;Thankfully we found &lt;strong&gt;react-icons&lt;/strong&gt;, a NPM package with all free Font Awesome (and other icon packages too!) in a SVG format and exported as React Components on a ES Module format.&lt;/p&gt;

&lt;p&gt;You can then &lt;strong&gt;import only the icons you need&lt;/strong&gt; and webpack will remove all other icons from the bundle. The result? Our CSS has is now &lt;strong&gt;~68kB smaller&lt;/strong&gt;. Not to mention that we don’t need to download external fonts anymore. This change was the biggest contributor on reducing the CSS size on Fider.&lt;/p&gt;

&lt;p&gt;Want see how? Check out this &lt;a href="https://github.com/getfider/fider/pull/631" rel="noopener noreferrer"&gt;PR #631&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Switching from big to small NPM packages
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"NPM is like a lego store full of building blocks that you can just pick whichever one you like. You don’t pay for the package you install, but your users pay for the byte size that it adds to your application. Choose wisely." - &lt;a class="mentioned-user" href="https://dev.to/goenning"&gt;@goenning&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While using the Bundle Analyzer we found that markdown-it alone was consuming ~40% of our vendor bundle. We have then decided to go shopping on NPM and look for an alternative markdown parser. The goal was to find a package that was smaller, well maintained and had all the features we needed.&lt;/p&gt;

&lt;p&gt;We’ve been using &lt;a href="https://bundlephobia.com/" rel="noopener noreferrer"&gt;bundlephobia.com&lt;/a&gt; to analyse the byte size of any NPM package before installing it. We have switched from markdown-it to marked, which &lt;strong&gt;reduced ~63kB from our vendor bundle&lt;/strong&gt; with minimal API change. &lt;/p&gt;

&lt;p&gt;Curious about it? Check out &lt;a href="https://github.com/getfider/fider/pull/643" rel="noopener noreferrer"&gt;PR #643&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can also compare these two packages on bundlephobia:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bundlephobia.com/result?p=marked@0.5.2" rel="noopener noreferrer"&gt;https://bundlephobia.com/result?p=marked@0.5.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bundlephobia.com/result?p=markdown-it@8.4.2" rel="noopener noreferrer"&gt;https://bundlephobia.com/result?p=markdown-it@8.4.2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think twice before adding a large package. Do you really need it? Can your team implement a simpler alternative? If not, can you find another package that does the same job with less bytes? Ultimately, you can still add the NPM package and load it asynchronously like we did with react-toastify mentioned above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimising the main bundle is crucial
&lt;/h2&gt;

&lt;p&gt;Imagine that you have an application doing code splitting by route. It’s already running in production and you commit a change to your Dashboard route component. You might think that Webpack will only generate a different file for the bundle that contain the Dashboard route, correct? &lt;/p&gt;

&lt;p&gt;Well, that’s not what actually happens.&lt;/p&gt;

&lt;p&gt;Webpack will &lt;strong&gt;ALWAYS&lt;/strong&gt; regenerate the main bundle if something else changes in your application. The reason being that the main bundle is a pointer to all other bundles. If the hash of another bundle has changed, the main bundle has to change its content so that it now points to the new hash of the Dashboard bundle. Makes sense?&lt;/p&gt;

&lt;p&gt;So if your main bundle contains not only the pointers, but also a lot of common components like Buttons, Toggle, Grids and Tabs, you’re basically forcing the browser to redownload something that has not changed.&lt;/p&gt;

&lt;p&gt;Use the webpack bundle analyzer to understand what’s inside your main bundle. You can then apply some of the techniques we’ve mentioned above to reduce the main bundle size.&lt;/p&gt;

&lt;h2&gt;
  
  
  TSLib (TypeScript only)
&lt;/h2&gt;

&lt;p&gt;When compiling TypeScript code to ES5, the TypeScript Compiler will also emit some helper functions to the output JavaScript file. This process ensures that the code we wrote in TypeScript is compatible with older browsers that doesn’t support ES6 features like Classes and Generators.&lt;/p&gt;

&lt;p&gt;These helper functions are very small, but when there are many TypeScript files, these helper functions will be present on every file that uses a non-ES5 code. Webpack won’t be able to tree shake it and the final bundle will contain multiple occurrences of the very same code. The result? A slightly bigger bundle.&lt;/p&gt;

&lt;p&gt;Thankfully there’s a solution for this. There is a NPM package called &lt;strong&gt;tslib&lt;/strong&gt; that contains all the helper functions needed by TypeScript. We can then tell the compiler to import the helper functions from the tslib package instead of emitting it to the output JavaScript file. This is done by setting &lt;strong&gt;importHelpers: true&lt;/strong&gt; on the &lt;strong&gt;tsconfig.json&lt;/strong&gt; file. Don’t forget to install tslib with &lt;strong&gt;npm install tslib —save&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;That’s all! &lt;/p&gt;

&lt;p&gt;The amount of bytes this can reduce from the bundle will depend on the amount of non-ES5 files, which can be a lot on a React app if most of the Components are classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next billions users
&lt;/h2&gt;

&lt;p&gt;Are you ready for &lt;a href="https://developers.google.com/web/billions/" rel="noopener noreferrer"&gt;the next billion users&lt;/a&gt;? Think about all the potential users of your app that currently struggle to use it on a low-cost device and slower network.&lt;/p&gt;

&lt;p&gt;Reducing the byte size of our bundles has a direct impact on the performance of our applications and can help us make it more accessible to everyone. Hopefully this post can you help on this journey.&lt;/p&gt;

&lt;p&gt;Thank you for reading!&lt;/p&gt;

</description>
      <category>webpack</category>
      <category>webdev</category>
      <category>react</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to retry when React lazy fails</title>
      <dc:creator>Guilherme Oenning</dc:creator>
      <pubDate>Mon, 19 Nov 2018 17:09:09 +0000</pubDate>
      <link>https://dev.to/goenning/how-to-retry-when-react-lazy-fails-mb5</link>
      <guid>https://dev.to/goenning/how-to-retry-when-react-lazy-fails-mb5</guid>
      <description>&lt;p&gt;React 16.6 has been released and it's now easier than ever to do code split within our React applications by using lazy and Suspense.&lt;/p&gt;

&lt;p&gt;If you don't know what I'm talking about, you should definitely read this &lt;a href="https://reactjs.org/blog/2018/10/23/react-v-16-6.html" rel="noopener noreferrer"&gt;https://reactjs.org/blog/2018/10/23/react-v-16-6.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a few days monitoring a production application that is using lazy, I noticed some client-side errors like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loading chunk 6 failed. (error: https://.../6.4e464a072cc0e5e27a07.js)
Loading CSS chunk 6 failed. (https://.../6.38a8cd5e9daba617fb66.css)    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I don't actually know why, but I can only assume this is related to the user network. Maybe they are on a slow 3G and there was a network hiccup? That's not a rare event, right?&lt;/p&gt;

&lt;p&gt;Alright, if that's true, how do we solve that?&lt;/p&gt;

&lt;p&gt;We can do the same thing that everyone does when a network request fails: retry it! 😄&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Did you know that the &lt;strong&gt;import(...)&lt;/strong&gt; function that we use on lazy is just a function that returns a Promise? Which basically means that you can chain it just like any other Promise.&lt;/p&gt;

&lt;p&gt;Below you can find a basic implementation of a retry function.&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retriesLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&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="nf"&gt;setTimeout&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retriesLeft&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// reject('maximum retries exceeded');&lt;/span&gt;
            &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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="c1"&gt;// Passing on "reject" is the important part&lt;/span&gt;
          &lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;retriesLeft&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;interval&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Source: &lt;a href="https://gist.github.com/briancavalier/842626" rel="noopener noreferrer"&gt;https://gist.github.com/briancavalier/842626&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we just need to apply it to our lazy import.&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="c1"&gt;// Code split without retry login&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProductList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&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;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./path/to/productlist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Code split with retry login&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProductList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;retry&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;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./path/to/productlist&lt;/span&gt;&lt;span class="dl"&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 the browser fails to download the module, it'll try again 5 times with a 1 second delay between each attempt. If even after 5 tries it import it, then an error is thrown.&lt;/p&gt;

&lt;p&gt;That's all! 🎉&lt;/p&gt;

&lt;p&gt;Thanks!&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
