{"@attributes":{"version":"2.0"},"channel":{"title":"DEV Community: Pawe\u0142 Kowalski","description":"The latest articles on DEV Community by Pawe\u0142 Kowalski (@pavelloz).","link":"https:\/\/dev.to\/pavelloz","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%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg","title":"DEV Community: Pawe\u0142 Kowalski","link":"https:\/\/dev.to\/pavelloz"},"language":"en","item":[{"title":"Background images in TailwindCSS - the clean and easy way","pubDate":"Thu, 14 Oct 2021 12:12:26 +0000","link":"https:\/\/dev.to\/platformos\/background-images-in-tailwindcss-the-clean-and-easy-way-gho","guid":"https:\/\/dev.to\/platformos\/background-images-in-tailwindcss-the-clean-and-easy-way-gho","description":"<p>My biggest issue with TailwindCSS was the ugly way of using background images with it. I had to inline <code>background-url<\/code> property and it was not clean. And I know I was not alone, because other frontend developers said the same thing. It looked similar to this:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight jsx\"><code><span class=\"p\">&lt;<\/span><span class=\"nt\">section<\/span> <span class=\"na\">class<\/span><span class=\"p\">=<\/span><span class=\"s\">\"bg-accent-dark bg-cover\"<\/span>\n  <span class=\"na\">style<\/span><span class=\"p\">=<\/span><span class=\"s\">\"background-image: url({{ 'images\/home\/hero.jpg' | asset_url }})\"<\/span><span class=\"p\">&gt;<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Note: We use liquid to generate url to an image placed on CDN. <\/p>\n\n<p>Couple days ago, when I was doing six different themes in TailwindCSS using six different configs (I will report my findings in another article) I discovered that it can be simplified. Because CSS file is also on CDN, and the background-image can be customized in TailwindCSS config (<a href=\"https:\/\/tailwindcss.com\/docs\/background-image#background-images\" rel=\"noopener noreferrer\">read more<\/a>), it can be replaced with simple <code>bg-hero<\/code> after adding its definition into theme extend section:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight jsx\"><code><span class=\"nx\">backgroundImage<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n  <span class=\"dl\">'<\/span><span class=\"s1\">hero<\/span><span class=\"dl\">'<\/span><span class=\"p\">:<\/span> <span class=\"dl\">\"<\/span><span class=\"s2\">url('..\/images\/home\/hero.jpg')<\/span><span class=\"dl\">\"<\/span>\n<span class=\"p\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Now, our hero element HTML looks like this:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight jsx\"><code><span class=\"p\">&lt;<\/span><span class=\"nt\">section<\/span> <span class=\"na\">class<\/span><span class=\"p\">=<\/span><span class=\"s\">\"bg-accent-dark bg-cover bg-hero\"<\/span><span class=\"p\">&gt;<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>And I think its much cleaner! Reading documentation paid off :)<\/p>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/using-webp-in-your-existing-webpage-809\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Using WebP in Your Existing Webpage<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 17 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#design<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/optimizing-images-for-the-web-18gc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Optimizing Images For The Web<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Apr 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n        <span class=\"ltag__link__tag\">#node<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":["tailwindcss","css","beginners"]},{"title":"We Are Switching From TestCafe to CodeceptJS \u2013 Here\u2019s Why","pubDate":"Tue, 28 Sep 2021 08:02:24 +0000","link":"https:\/\/dev.to\/platformos\/we-are-switching-from-testcafe-to-codeceptjs-here-s-why-39ml","guid":"https:\/\/dev.to\/platformos\/we-are-switching-from-testcafe-to-codeceptjs-here-s-why-39ml","description":"<p>We have been using and promoting TestCafe at platformOS for the past couple of years with great success. Because a lot of people will write tests and maintain them over a long time, an End-to-End framework has to come with some specific requirements:<\/p>\n\n<ol>\n<li>Easy to remember and type out API<\/li>\n<li>Good waiting mechanisms (for XHR requests, animations)<\/li>\n<li>Extendibility, page object support, helpers support<\/li>\n<li>Good search in documentation to quickly reference less used APIs<\/li>\n<li>Run properly in Docker and\/or GitHub Actions<\/li>\n<\/ol>\n\n<p>TestCafe is scoring high on the above areas, I would say averaging around 7.5\/10, which means there is still room for improvement.<\/p>\n\n<p>Even though we have been happy with TestCafe, last year when I stumbled upon a new contender, CodeceptJS, I decided to give it a shot on our\u00a0<a href=\"https:\/\/documentation.platformos.com\/\">documentation<\/a> and <a href=\"https:\/\/www.platformos.com\/\">marketing<\/a> sites. It delivered excellent developer performance. It was enough to dive deeper into its documentation and expand our test suites to include some more test cases.<\/p>\n\n<h2>\n  \n  \n  1. Test API\n<\/h2>\n\n<p>Very often when writing TestCafe tests, we had to resort to vanilla JS and DOM operations. One of the most frustrating examples was to get some text from an element and then compare it to another. It was too much work and I never could see a reason why TestCafe had no API for that. CodeceptJS has a lot more API helpers to avoid these complications and diverging into vanilla JS. Below, I give you some examples of TestCafe scenarios converted to CodeceptJS ones.<\/p>\n\n<h3>\n  \n  \n  Checking if correct breadcrumbs links are present on a page\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"c1\">\/\/ TestCafe<\/span>\n<span class=\"nx\">test<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Breadcrumbs are showing up<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"k\">async<\/span> <span class=\"nx\">t<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"p\">{<\/span>\n  <span class=\"k\">await<\/span> <span class=\"nx\">t<\/span><span class=\"p\">.<\/span><span class=\"nx\">navigateTo<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">\/api-reference\/liquid\/introduction<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"k\">await<\/span> <span class=\"nx\">t<\/span><span class=\"p\">.<\/span><span class=\"nx\">expect<\/span><span class=\"p\">(<\/span><span class=\"nx\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">.breadcrumbs a<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nx\">withText<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Documentation<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nx\">exists<\/span><span class=\"p\">).<\/span><span class=\"nx\">ok<\/span><span class=\"p\">();<\/span>\n  <span class=\"k\">await<\/span> <span class=\"nx\">t<\/span><span class=\"p\">.<\/span><span class=\"nx\">expect<\/span><span class=\"p\">(<\/span><span class=\"nx\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">.breadcrumbs a<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nx\">withText<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">API Reference<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nx\">exists<\/span><span class=\"p\">).<\/span><span class=\"nx\">ok<\/span><span class=\"p\">();<\/span>\n  <span class=\"k\">await<\/span> <span class=\"nx\">t<\/span><span class=\"p\">.<\/span><span class=\"nx\">expect<\/span><span class=\"p\">(<\/span><span class=\"nx\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">.breadcrumbs a<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nx\">withText<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Introduction<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nx\">exists<\/span><span class=\"p\">).<\/span><span class=\"nx\">ok<\/span><span class=\"p\">();<\/span>\n<span class=\"p\">});<\/span>\n\n<span class=\"c1\">\/\/ CodeceptJS<\/span>\n<span class=\"nx\">Scenario<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Are showing up<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"p\">({<\/span> <span class=\"nx\">I<\/span> <span class=\"p\">})<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"p\">{<\/span>\n  <span class=\"nx\">I<\/span><span class=\"p\">.<\/span><span class=\"nx\">amOnPage<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">\/api-reference\/liquid\/introduction<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"nx\">I<\/span><span class=\"p\">.<\/span><span class=\"nx\">see<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Documentation<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">.breadcrumbs<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"nx\">I<\/span><span class=\"p\">.<\/span><span class=\"nx\">see<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">API Reference<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">.breadcrumbs<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"nx\">I<\/span><span class=\"p\">.<\/span><span class=\"nx\">see<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Introduction<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">.breadcrumbs<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n<span class=\"p\">});<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<blockquote>\n<h4>\n  \n  \n  If this is interesting to you <a href=\"https:\/\/www.platformos.com\/blog\/post\/we-are-switching-from-testcafe-to-codeceptjs-here-s-why\">read rest of this article on our blog<\/a>.\n<\/h4>\n<\/blockquote>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/platformos-documentation-site-webpack-setup-93l\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>platformOS Documentation Site Webpack Setup<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Dec 4 '20 \u30fb 6 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#platformos<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-performance-tips-for-your-next-project-2bdm\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Performance Tips for Your Next Project<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 29 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/should-you-always-care-about-your-website-size-2jcc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Should You Always Care about Your Website Size?<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 12 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webdev<\/span>\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":["javascript","testing"]},{"title":"How We sped Up Our Webpack (TailwindCSS) 7 times!","pubDate":"Wed, 07 Apr 2021 17:20:31 +0000","link":"https:\/\/dev.to\/platformos\/how-we-sped-up-our-webpack-tailwindcss-7-times-1c05","guid":"https:\/\/dev.to\/platformos\/how-we-sped-up-our-webpack-tailwindcss-7-times-1c05","description":"<p>In the last article about build speed optimization, we described how we went from 64 seconds to 17 seconds on our Webpack build (measured on GithubActions, a pretty slow environment CPU-wise). Just as we did it and managed to write an article about it to share the knowledge, something amazing happened: TailwindCSS\/JIT.<\/p>\n\n<p>JIT (short from Just In Time) for TailwindCSS is a much more performant way of generating the TailwindCSS output file. Instead of generating a big (sometimes 10MB+) CSS file and then using PurgeCSS to remove unnecessary classes, it only generates what is needed in the first place. This makes PurgeCSS and many other speed optimization techniques in TailwindCSS unnecessary. It is very fast no matter what config you use, and the output file size is still optimal.<\/p>\n\n<p>We jumped into experimenting with JIT as soon as it got a beta release, so there were some bugs, but now we consider it good enough for production, hence this article.<\/p>\n\n<blockquote>\n<p>Result: Webpack build took <strong>8.9<\/strong> seconds (down from 17)<\/p>\n<\/blockquote>\n\n<p>As a nice side-effect of using JIT, our TailwindCSS config became much smaller because we don't need to disable modules, override theme colors, spacing, etc. Now everything is taken care of by JIT during runtime. It is so fast that development became a breeze.<\/p>\n\n<p><a href=\"https:\/\/tailwindcss.com\/docs\/just-in-time-mode#enabling-jit-mode\">Read more on JIT in the official TailwindCSS documentation<\/a><\/p>\n\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/platformos-documentation-site-webpack-setup-93l\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>platformOS Documentation Site Webpack Setup<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Dec 4 '20 \u30fb 6 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#platformos<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-performance-tips-for-your-next-project-2bdm\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Performance Tips for Your Next Project<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 29 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/should-you-always-care-about-your-website-size-2jcc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Should You Always Care about Your Website Size?<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 12 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webdev<\/span>\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":["performance","webpack","javascript"]},{"title":"How We Sped Up Our Webpack (TailwindCSS) Build By 74%","pubDate":"Mon, 22 Mar 2021 10:23:55 +0000","link":"https:\/\/dev.to\/platformos\/how-we-sped-up-our-webpack-tailwindcss-build-by-57-1hci","guid":"https:\/\/dev.to\/platformos\/how-we-sped-up-our-webpack-tailwindcss-build-by-57-1hci","description":"<p>Since Github Actions became a thing, I became interested in saving precious seconds in our plan by optimizing assets build time. As long as it didn't exceed 60 seconds, there was little motivation, but once it did, I said enough is enough.<\/p>\n\n<p>Our build step runs two commands:<\/p>\n\n<ul>\n<li><code>npm ci<\/code><\/li>\n<li><code>npm run build<\/code><\/li>\n<\/ul>\n\n<p>We use babel and TailwindCSS. Using simple methods, I discovered that CSS and JS take about the same time to build. <\/p>\n\n<blockquote>\n<p>Benchmark: Webpack build took <strong>64<\/strong> seconds, whole build took <strong>91<\/strong> seconds.<\/p>\n<\/blockquote>\n\n<h2>\n  \n  \n  Replacing babel-loader + terser with <a href=\"https:\/\/github.com\/privatenumber\/esbuild-loader\" rel=\"noopener noreferrer\">esbuild loader<\/a>\n<\/h2>\n\n<p>The first thing I did was replace <code>babel-loader<\/code> and Terser (minification tool) with <a href=\"https:\/\/github.com\/privatenumber\/esbuild-loader\" rel=\"noopener noreferrer\">esbuild-loader<\/a>. This made our JS compile around 12 times faster, it went down to 1.4 seconds. It was a good start.<\/p>\n\n<blockquote>\n<p>Result: Webpack build took <strong>40<\/strong> seconds (down from 64), whole build took <strong>65<\/strong> seconds.<\/p>\n<\/blockquote>\n\n<h2>\n  \n  \n  Configuring Tailwind\n<\/h2>\n\n<p>The second optimization had to do something with CSS because that's where most of the build time now lay. I visited the TailwindCSS documentation to find out how to decrease build size.<\/p>\n\n<p>The template we base our projects on uses a style guide with predefined colors, sizes, etc., so we don't need some of the configuration from TailwindCSS. Knowing this, here's what I did:<\/p>\n\n<ol>\n<li>Moved <code>colors<\/code> configuration outside <code>extend<\/code>, which disabled all the built-in colors from the build. This made a huge difference in development file size (10.5MB \u2192 4MB).<\/li>\n<li>Moved <code>spacing<\/code> configuration outside <code>extend<\/code>\n<\/li>\n<li>Disabled corePlugins<\/li>\n<\/ol>\n\n<p>I had to bring some of these back because we used them in a couple of places. After these changes, I considered TailwindCSS optimization done. <\/p>\n\n<p>The file size of development CSS went down from 10.5MB to 3.9MB, which is a big deal (depending on the developer's connection speed) if you are sending your CSS on every CSS change. It does not happen often when working with TailwindCSS, but it was still a welcome improvement for everyone using our <code>pos-cli sync<\/code> command.  <\/p>\n\n<blockquote>\n<p>Result: Webpack build took <strong>26<\/strong> seconds (down from 40), whole build took <strong>49<\/strong> seconds.<\/p>\n<\/blockquote>\n\n\n\n\n<p>Read about TailwindCSS optimization in their documentation:<\/p>\n\n<ul>\n<li><a href=\"https:\/\/tailwindcss.com\/docs\/optimizing-for-production\" rel=\"noopener noreferrer\">https:\/\/tailwindcss.com\/docs\/optimizing-for-production<\/a><\/li>\n<li><a href=\"https:\/\/tailwindcss.com\/docs\/configuration#theme\" rel=\"noopener noreferrer\">https:\/\/tailwindcss.com\/docs\/configuration<\/a><\/li>\n<\/ul>\n\n<h2>\n  \n  \n  Disabling PostCSS processing\n<\/h2>\n\n<p>The last step was to disable PostCSS processing of CSS pulled in from <code>node_modules<\/code>.<\/p>\n\n<p>Before, it was pretty naive \u2014 everything went through all the CSS loaders:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"pi\">{<\/span>\n  <span class=\"nv\">test<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">\/(\\.css)$\/<\/span><span class=\"pi\">,<\/span>\n  <span class=\"nv\">use<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">[<\/span>\n    <span class=\"nv\">MiniCssExtractPlugin.loader<\/span><span class=\"pi\">,<\/span>\n    <span class=\"pi\">{<\/span> <span class=\"nv\">loader<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">css-loader'<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">options<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">{<\/span> <span class=\"nv\">url<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">false<\/span> <span class=\"pi\">}<\/span> <span class=\"pi\">},<\/span>\n    <span class=\"s1\">'<\/span><span class=\"s\">postcss-loader'<\/span><span class=\"pi\">,<\/span>\n  <span class=\"pi\">],<\/span>\n<span class=\"pi\">}<\/span><span class=\"err\">,<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>I split it into two so that <code>node_modules<\/code> are processed only by <code>css-loader<\/code>, and application CSS is processed by PostCSS first.<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"pi\">{<\/span>\n  <span class=\"nv\">test<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">\/(\\.css)$\/<\/span><span class=\"pi\">,<\/span>\n  <span class=\"nv\">include<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">\/node_modules\/<\/span><span class=\"pi\">,<\/span>\n  <span class=\"nv\">use<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">[<\/span>\n    <span class=\"nv\">MiniCssExtractPlugin.loader<\/span><span class=\"pi\">,<\/span>\n    <span class=\"pi\">{<\/span> <span class=\"nv\">loader<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">css-loader'<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">options<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">{<\/span> <span class=\"nv\">url<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">false<\/span> <span class=\"pi\">}<\/span> <span class=\"pi\">}<\/span>\n  <span class=\"pi\">],<\/span>\n<span class=\"pi\">}<\/span><span class=\"err\">,<\/span>\n<span class=\"pi\">{<\/span>\n  <span class=\"nv\">test<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">\/(\\.css)$\/<\/span><span class=\"pi\">,<\/span>\n  <span class=\"nv\">exclude<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">\/node_modules\/<\/span><span class=\"pi\">,<\/span>\n  <span class=\"nv\">use<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">[<\/span>\n    <span class=\"nv\">MiniCssExtractPlugin.loader<\/span><span class=\"pi\">,<\/span>\n    <span class=\"pi\">{<\/span> <span class=\"nv\">loader<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">css-loader'<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">options<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">{<\/span> <span class=\"nv\">url<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">false<\/span> <span class=\"pi\">}<\/span> <span class=\"pi\">},<\/span>\n    <span class=\"s1\">'<\/span><span class=\"s\">postcss-loader'<\/span><span class=\"pi\">,<\/span>\n  <span class=\"pi\">],<\/span>\n<span class=\"pi\">}<\/span><span class=\"err\">,<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<blockquote>\n<p>Result: Webpack build took <strong>17<\/strong> seconds (down from 26), whole build took <strong>39<\/strong> seconds.<\/p>\n<\/blockquote>\n<h3>\n  \n  \n  That's all :)\n<\/h3>\n\n<p>This journey was one of those creative ones where you fiddle around, find gains in various places, and they add up. In this case, they added up to 52 seconds of savings for every build. And we build a lot, so this improves our developer experience and lowers the cost of CI.<\/p>\n\n<div class=\"table-wrapper-paragraph\"><table>\n<thead>\n<tr>\n<th>Action<\/th>\n<th>Webpack build time (seconds)<\/th>\n<th>Whole build time (seconds)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>At the start<\/td>\n<td>64<\/td>\n<td>91<\/td>\n<\/tr>\n<tr>\n<td>Using esbuild loader<\/td>\n<td>40<\/td>\n<td>65<\/td>\n<\/tr>\n<tr>\n<td>Configuring Tailwind<\/td>\n<td>26<\/td>\n<td>49<\/td>\n<\/tr>\n<tr>\n<td>Disabling PostCSS processing<\/td>\n<td>17<\/td>\n<td>39<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<p>In the following article, I will report how development speedup is going because the TailwindCSS team just released the experimental <a href=\"https:\/\/github.com\/tailwindlabs\/tailwindcss-jit\" rel=\"noopener noreferrer\">TailwindCSS JIT<\/a>, which might improve live development even further.<\/p>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/platformos-documentation-site-webpack-setup-93l\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>platformOS Documentation Site Webpack Setup<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Dec 4 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#platformos<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-performance-tips-for-your-next-project-2bdm\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Performance Tips for Your Next Project<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 29 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/should-you-always-care-about-your-website-size-2jcc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Should You Always Care about Your Website Size?<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 12 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webdev<\/span>\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":["performance","webpack","tailwindcss"]},{"title":"How to catch Front-end performance regressions?","pubDate":"Tue, 29 Dec 2020 15:14:01 +0000","link":"https:\/\/dev.to\/platformos\/how-to-catch-front-end-performance-regressions-4dl","guid":"https:\/\/dev.to\/platformos\/how-to-catch-front-end-performance-regressions-4dl","description":"<p>If you try to take care of your visitors by making your website fast and light, you probably measure its speed using some tool. Chances are you use one of these free tools:<\/p>\n\n<ul>\n<li><a href=\"https:\/\/web.dev\/measure\/\">Lighthouse<\/a><\/li>\n<li><a href=\"https:\/\/webpagetest.org\/\">WebpageTest<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/speed\/pagespeed\/insights\/\">PageSpeed Insights<\/a><\/li>\n<\/ul>\n\n<p>Today I will show you how to:<\/p>\n\n<ul>\n<li>Run Lighthouse easily from your browser on your current page<\/li>\n<li>Save report to JSON file<\/li>\n<li>Compare past report with current report to see if your page is going in the right direction<\/li>\n<\/ul>\n\n<h3>\n  \n  \n  What you will need\n<\/h3>\n\n<p>Only two things are needed to compare past report with current one.<\/p>\n\n<p>Lighthouse extension for your browser:<\/p>\n\n<ul>\n<li><a href=\"https:\/\/addons.mozilla.org\/en-US\/firefox\/addon\/google-lighthouse\/\">Firefox<\/a><\/li>\n<li><a href=\"https:\/\/chrome.google.com\/webstore\/detail\/lighthouse\/blipmdconlkpinefehnmjammfjpmpbjk\">Chrome<\/a><\/li>\n<\/ul>\n\n<p>And a website that will compare two reports: <\/p>\n\n<ul>\n<li><a href=\"https:\/\/googlechrome.github.io\/lighthouse-ci\/viewer\/\">Lighthouse CI Diff<\/a><\/li>\n<\/ul>\n\n<h3>\n  \n  \n  Generate report\n<\/h3>\n\n<p>After you have installed Lighthouse extension go to your website. Open extension by clicking on its icon, change settings to include all reports and use Desktop strategy, and click generate report.<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--BKQhmdAr--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/dqf1vyu85xssed6gk9ix.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--BKQhmdAr--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/dqf1vyu85xssed6gk9ix.png\" alt=\"Lighthouse Extension\" width=\"544\" height=\"1024\"><\/a><\/p>\n\n<blockquote>\n<p>Note: You can use mobile strategy, or skip PWA - we do. The important thing is to stick to the settings because if Lighthouse report is generated using different settings, compare tool might have issues with detecting changes.<\/p>\n<\/blockquote>\n\n<p>It should look something like this:<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--zloA2P6p--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/wmwfnfoq4hbekpjhsbmf.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--zloA2P6p--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/wmwfnfoq4hbekpjhsbmf.png\" alt=\"Example report\" width=\"880\" height=\"258\"><\/a><\/p>\n\n<p>Now you can export this report to JSON file for future reference. In top-right corner there is a \"three dot\" menu, click it and generate JSON report. Save it in a safe place - you will need it.<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--93jONvhw--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/4h6r7uyd3r7zv0hluqip.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--93jONvhw--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/4h6r7uyd3r7zv0hluqip.png\" alt=\"Saving report\" width=\"398\" height=\"556\"><\/a><\/p>\n\n<p>When you have two reports, ideally generated between some changes, you can go to <a href=\"https:\/\/googlechrome.github.io\/lighthouse-ci\/viewer\/\">https:\/\/googlechrome.github.io\/lighthouse-ci\/viewer\/<\/a> and upload both of them to compare. Upload the older one on the left (Base), and newer one on the right (Compare) - this way it will tell you if you made progress or not.<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--OedLyNSL--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/mni386txfvm96e94fcxn.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--OedLyNSL--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/mni386txfvm96e94fcxn.png\" alt=\"Lighthouse CI Diff tool\" width=\"880\" height=\"223\"><\/a><\/p>\n\n<p>This is how generated comparison looks for our last two builds on our <a href=\"https:\/\/documentation.platformos.com\">platformOS documentation site<\/a>.<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XnUuDAur--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/jxq1pr0cj4aab96bazyb.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XnUuDAur--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/jxq1pr0cj4aab96bazyb.png\" alt=\"Example comparison\" width=\"880\" height=\"519\"><\/a><\/p>\n\n<h3>\n  \n  \n  Future improvements\n<\/h3>\n\n<p>In the next post I will write how you can use Github Actions to do the reports automatically. It will give you link to comparison report on every pull request, so you can skip the boring parts and get to the good stuff. :)<\/p>\n\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you like web performance, take a look at my other articles, you might find some inspiration there how to make progress without much effort. And if you really like web performance, consider following me :)<\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20 \u30fb 6 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-performance-tips-for-your-next-project-2bdm\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Performance Tips for Your Next Project<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 29 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n","category":["webperf","beginners"]},{"title":"How platformOS uses mixpanel while keeping users privacy?","pubDate":"Sun, 20 Dec 2020 14:44:04 +0000","link":"https:\/\/dev.to\/platformos\/how-platformos-uses-mixpanel-and-keeping-users-privacy-118n","guid":"https:\/\/dev.to\/platformos\/how-platformos-uses-mixpanel-and-keeping-users-privacy-118n","description":"<p>We at platformOS value our visitors privacy, time and money. And because we live by our values, we always dive deeper into technologies to manifest them. Today we will dive look into how we use mixpanel without breaking our website's performance or sacrificing our visitors privacy.<\/p>\n\n<p>We use mixpanel to learn where our visitors are confused, leaving our pages<\/p>\n\n<h2>\n  \n  \n  Why mixpanel\n<\/h2>\n\n<p>Mixpanel is an industry standard when it comes to getting to know what visitors are doing on your site.<br>\nIt does not track everything like Google Analytics, but only what you tell it to. At least we hoped that would be the case, but looking at the documentation and training materials led us to believe that it is actually gathering more data than we were comfortable with giving or needed.<\/p>\n\n<p>Having said that, mixpanel has a very comprehensive set of tools to dive deeper into where your visitors journey on your website. This is very important for onboarding process because you can focus on exactly the part that is a bottleneck, so you don't waste time optimizing the wrong part of it.<\/p>\n<h3>\n  \n  \n  How mixpanel is usually implemented\n<\/h3>\n\n<p>Just like most third-party services mixpanel usually is implemented on the frontend, using javascript. This has a lot of consequences, no matter if you load the script from your CDN or from mixpanel CDN.<\/p>\n\n<ul>\n<li>It has to load - your page becomes bigger (around 80KB bigger)<\/li>\n<li>It has to parse and execute - your page becomes slower, especially on slower devices (mobile)<\/li>\n<li>You don't really know what it does (or track), unless you are inspecting mixpanel javascript client codebase before installation and every update<\/li>\n<li>\n<a href=\"https:\/\/developer.mixpanel.com\/docs\/javascript\">According to documentation<\/a> turning on GDPR compliance is an opt-in, so this might create additional friction and work to be done<\/li>\n<li>AdBlockers have mixpanel CDN blocked by default, so there will be gaps in data. How big of a gaps? Well, some sources report 50% gaps, which is big enough to not rely on client-side measurements.<\/li>\n<\/ul>\n\n<p><a href=\"https:\/\/twitter.com\/garybernhardt\/status\/1338679291182080000\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--yIZJ_o6J--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/esx4kv3chnptk3x8u8ty.jpg\" alt=\"Tweet\" width=\"880\" height=\"478\"><\/a><\/p>\n\n<p>All those disadvantages are directly against our values. But mixpanel is good enough product to do the research and our great engineers found a way to use mixpanel without any javascript.<\/p>\n<h2>\n  \n  \n  How we use mixpanel\n<\/h2>\n\n<p>Sending HTTP requests in platformOS is very simple, and we decided to leverage that in our implementation.<\/p>\n\n<p>First we defined API call that will do the network call:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"nn\">---<\/span>\n<span class=\"na\">name<\/span><span class=\"pi\">:<\/span> <span class=\"s\">mixpanel_create_event<\/span>\n<span class=\"na\">to<\/span><span class=\"pi\">:<\/span> <span class=\"s\">https:\/\/api.mixpanel.com\/track<\/span>\n<span class=\"na\">format<\/span><span class=\"pi\">:<\/span> <span class=\"s\">https<\/span>\n<span class=\"na\">request_type<\/span><span class=\"pi\">:<\/span> <span class=\"s\">POST<\/span>\n<span class=\"na\">callback<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">&gt;<\/span>\n  <span class=\"s\">{%- assign response_data = response.body | to_hash -%}<\/span>\n  <span class=\"s\">{% if response_data.error %}<\/span>\n    <span class=\"s\">{%- log response_data.error, type: 'modules\/monitoring\/mixpanel_create_event' -%}<\/span>\n  <span class=\"s\">{% endif %}<\/span>\n<span class=\"na\">request_headers<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">&gt;<\/span>\n  <span class=\"s\">{% if data %}<\/span>\n    <span class=\"s\">{<\/span>\n      <span class=\"s\">\"Content-Type\": \"application\/x-www-form-urlencoded\"<\/span>\n    <span class=\"s\">}<\/span>\n  <span class=\"s\">{% endif %}<\/span>\n<span class=\"s\">---<\/span>\n<span class=\"s\">data={{ data }}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Everything between <code>---<\/code> is definition of the api call, everything below, is the body of the request that will be sent. Additionally, <code>callback<\/code> is executed after the API call has been sent. In our case it is logging error if mixpanel server returned one.<\/p>\n\n<p>Having defined API call, we need to execute whenever we need to, with proper data. Here we created another abstraction to be able to pass different events from different sources:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"pi\">{<\/span><span class=\"err\">%<\/span> <span class=\"nv\">liquid<\/span>\n  <span class=\"nv\">graphql instance = 'modules\/monitoring\/instance' | dig<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">instance'<\/span>\n  <span class=\"nv\">unless data<\/span>\n    <span class=\"nv\">assign data = '<\/span><span class=\"pi\">{}<\/span><span class=\"s1\">'<\/span><span class=\"nv\"> <\/span><span class=\"s\">|<\/span><span class=\"nv\"> <\/span><span class=\"s\">parse_json<\/span>\n  <span class=\"s\">endunless<\/span>\n  <span class=\"s\">hash_assign<\/span><span class=\"nv\"> <\/span><span class=\"s\">data['<\/span><span class=\"nv\">distinct_id'<\/span><span class=\"err\">]<\/span> <span class=\"nv\">= instance.id<\/span>\n  <span class=\"nv\">hash_assign data<\/span><span class=\"pi\">[<\/span><span class=\"s1\">'<\/span><span class=\"s\">instance_id'<\/span><span class=\"pi\">]<\/span> <span class=\"nv\">= instance.id<\/span>\n  <span class=\"nv\">hash_assign data<\/span><span class=\"pi\">[<\/span><span class=\"s1\">'<\/span><span class=\"s\">token'<\/span><span class=\"pi\">]<\/span> <span class=\"nv\">= \"377404cb3e579051250ca9a2b129ea7b\"<\/span>\n<span class=\"nv\">%<\/span><span class=\"pi\">}<\/span>\n<span class=\"pi\">{<\/span><span class=\"err\">%<\/span> <span class=\"nv\">parse_json mixpanel_data %<\/span><span class=\"pi\">}<\/span>\n<span class=\"pi\">{<\/span>\n  <span class=\"s2\">\"<\/span><span class=\"s\">event\"<\/span><span class=\"pi\">:<\/span> <span class=\"s2\">\"<\/span><span class=\"s\">{{<\/span><span class=\"nv\"> <\/span><span class=\"s\">event_name<\/span><span class=\"nv\"> <\/span><span class=\"s\">}}\"<\/span><span class=\"pi\">,<\/span>\n  <span class=\"s2\">\"<\/span><span class=\"s\">properties\"<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">{{<\/span> <span class=\"nv\">data<\/span> <span class=\"pi\">}}<\/span>\n<span class=\"pi\">}<\/span>\n<span class=\"pi\">{<\/span><span class=\"err\">%<\/span> <span class=\"nv\">endparse_json %<\/span><span class=\"pi\">}<\/span>\n\n<span class=\"pi\">{<\/span><span class=\"err\">%<\/span> <span class=\"nv\">liquid<\/span>\n  <span class=\"nv\">graphql r = 'modules\/monitoring\/api_call'<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">data<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">mixpanel_data<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">template<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">modules\/monitoring\/mixpanel_create_event'<\/span>\n  <span class=\"nv\">log r<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">type<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">monitoring\/migration\/track_first_deploy'<\/span>\n  <span class=\"nv\">return r<\/span>\n<span class=\"nv\">%<\/span><span class=\"pi\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>To every event we attach some basic data, like:<\/p>\n\n<ul>\n<li>public token (required by mixpanel),<\/li>\n<li>instance id - to know which website sent the event<\/li>\n<\/ul>\n\n<p>And some variables that are passed down from different parts of the system, one is <code>event_name<\/code> which is just a name to filter them easier in the mixpanel dashboard, and <code>data<\/code> which is a JSON object that can contain anything we want, including user data, like id, email, browser data (extracted from user agent), etc.<\/p>\n\n<p>Now we can call this piece of code (partial) anywhere in our application, passing <code>event_name<\/code> and some <code>data<\/code>. Simplest example would be our <code>marketplace_install<\/code> event which is called every time someone installs our Marketplace Template for the first time.<\/p>\n\n<p>Event name is called <code>marketplace_install<\/code> to group all those events in one bucket. Our template has different variants (community site, ecommerce), so our <code>data<\/code> object identifies that so we know what type of marketplace our partners and clients are installing.<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"pi\">{<\/span><span class=\"err\">%<\/span> <span class=\"nv\">parse_json data %<\/span><span class=\"pi\">}<\/span>\n  <span class=\"pi\">{<\/span>\n    <span class=\"s2\">\"<\/span><span class=\"s\">marketplace\"<\/span><span class=\"pi\">:<\/span> <span class=\"s2\">\"<\/span><span class=\"s\">products\"<\/span>\n  <span class=\"pi\">}<\/span>\n<span class=\"pi\">{<\/span><span class=\"err\">%<\/span> <span class=\"nv\">endparse_json %<\/span><span class=\"pi\">}<\/span>\n\n<span class=\"pi\">{<\/span><span class=\"err\">%<\/span> <span class=\"nv\">liquid<\/span>\n  <span class=\"nv\">if context.location.host != 'getmarketplace-qa.staging.gapps.platformos.com'<\/span>\n    <span class=\"nv\">function res = 'modules\/monitoring\/commands\/track_event'<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">event_name<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">marketplace_install'<\/span><span class=\"pi\">,<\/span> <span class=\"nv\">data<\/span><span class=\"pi\">:<\/span> <span class=\"nv\">data<\/span>\n  <span class=\"nv\">endif<\/span>\n<span class=\"nv\">%<\/span><span class=\"pi\">}<\/span> \n<\/code><\/pre>\n\n<\/div>\n\n<h3>\n  \n  \n  Profits\n<\/h3>\n\n<p>Using mixpanel server-side has some consequences:<\/p>\n\n<ul>\n<li>There is no performance penalty at all on the frontend<\/li>\n<li>There is no performance penalty on the backend, because code can be run asynchronously, so visitor does not have to wait until data has been sent to mixpanel when browsing website<\/li>\n<li>Server only knows as much as browser tells it plus whatever logged in person provided, if anything<\/li>\n<li>Mixpanel does not run any code on neither browser side or server side, so hypothetical hack, our clients are safe. You might think it is far-fetched and a non factor, but history teaches us that hack\/leak is not a matter of \"if\" but \"when\", no matter how good of a security company implement. Thats one of the reasons we try to minimize data we keep, and give.<\/li>\n<\/ul>\n<h2>\n  \n  \n  Tracking across multiple sites\n<\/h2>\n\n<p>When you have multiple sites like we do (Documentation, Marketing page, Partner Portal) you might want to track visitor's journey across all of them. We had this thought too, but this would mean one thing: identifying single users.<\/p>\n\n<p>Long story short, it all comes down to <a href=\"https:\/\/pixelprivacy.com\/resources\/browser-fingerprinting\/\">fingerprinting<\/a> which is a standard practice for most big outlets, ad networks, etc. but is unacceptable by us. Not only it is sending a lot of javascript down the wire that visitors does not want, they would probably answer: \"NO\" if you asked them if they want to be fingerprinted, no matter what.<\/p>\n\n<p>Instead of tracking across multiple pages, we decided to restructure our onboarding flow a little bit to minimize jumping across different domains\/websites. It is more consistent visually and more performant for the visitor as well. <\/p>\n<h2>\n  \n  \n  Resources\n<\/h2>\n\n<p><a href=\"https:\/\/www.youtube.com\/watch?v=46SPsRshtXw\">[Video] What is mixpanel by Danny Lambert<\/a><\/p>\n\n<p><a href=\"https:\/\/developer.mixpanel.com\/docs\">Mixpanel Documentation<\/a><\/p>\n\n<p><a href=\"https:\/\/documentation.platformos.com\/\">platformOS Documentation<\/a><\/p>\n\n<p><a href=\"https:\/\/www.platformos.com\/\">platformOS Marketing page<\/a><\/p>\n\n<p><a href=\"https:\/\/github.com\/mdyd-dev\/product-marketplace-template\">platformOS product template github repository<\/a><\/p>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/using-webp-in-your-existing-webpage-809\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Using WebP in Your Existing Webpage<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 17 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#design<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/optimizing-images-for-the-web-18gc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Optimizing Images For The Web<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Apr 24 '20 \u30fb 8 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n        <span class=\"ltag__link__tag\">#node<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20 \u30fb 6 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":["beginners","analytics","privacy","performance"]},{"title":"platformOS Documentation Site Webpack Setup","pubDate":"Fri, 04 Dec 2020 18:25:30 +0000","link":"https:\/\/dev.to\/platformos\/platformos-documentation-site-webpack-setup-93l","guid":"https:\/\/dev.to\/platformos\/platformos-documentation-site-webpack-setup-93l","description":"<p>Webpack is a powerful tool. Back in the day, it had a reputation of being hard to learn and hard to use. Nowadays, it has excellent documentation, sensible defaults, plugins, and loader - all this to help you keep your config small while achieving great results.<\/p>\n\n<h3>\n  \n  \n  Important features in 2020\n<\/h3>\n\n<p>The most important features of webpack for our projects are:<\/p>\n\n<ul>\n<li>Tree shaking<\/li>\n<li>Code splitting<\/li>\n<li>Dynamic async loading chunks of code<\/li>\n<\/ul>\n\n<p>This article explains how we used webpack in our documentation for the past couple of years for easy development and performant production builds.<\/p>\n\n<p>The final webpack config looks like this:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>const path <span class=\"o\">=<\/span> require<span class=\"o\">(<\/span><span class=\"s1\">'path'<\/span><span class=\"o\">)<\/span><span class=\"p\">;<\/span>\nconst MiniCssExtractPlugin <span class=\"o\">=<\/span> require<span class=\"o\">(<\/span><span class=\"s1\">'mini-css-extract-plugin'<\/span><span class=\"o\">)<\/span><span class=\"p\">;<\/span>\nconst WebpackRequireFrom <span class=\"o\">=<\/span> require<span class=\"o\">(<\/span><span class=\"s1\">'webpack-require-from'<\/span><span class=\"o\">)<\/span><span class=\"p\">;<\/span>\nconst webpack <span class=\"o\">=<\/span> require<span class=\"o\">(<\/span><span class=\"s1\">'webpack'<\/span><span class=\"o\">)<\/span><span class=\"p\">;<\/span>\n\nconst production <span class=\"o\">=<\/span> process.env.NODE_ENV <span class=\"o\">===<\/span> <span class=\"s1\">'production'<\/span><span class=\"p\">;<\/span>\n\nmodule.exports <span class=\"o\">=<\/span> <span class=\"o\">{<\/span>\n  entry: <span class=\"o\">{<\/span>\n    <span class=\"s1\">'app'<\/span>: <span class=\"s1\">'.\/src\/app'<\/span>,\n    <span class=\"s1\">'graphql'<\/span>: <span class=\"s1\">'.\/modules\/graphql\/public\/assets\/graphql'<\/span>,\n  <span class=\"o\">}<\/span>,\n  output: <span class=\"o\">{<\/span>\n    chunkFilename: <span class=\"s1\">'[name].[chunkhash:3].js'<\/span>,\n    publicPath: <span class=\"s1\">''<\/span>,\n    path: path.resolve<span class=\"o\">(<\/span><span class=\"s1\">'app\/assets'<\/span><span class=\"o\">)<\/span>,\n  <span class=\"o\">}<\/span>,\n  module: <span class=\"o\">{<\/span>\n    rules: <span class=\"o\">[<\/span>\n      <span class=\"o\">{<\/span>\n        <span class=\"nb\">test<\/span>: \/<span class=\"o\">(<\/span><span class=\"se\">\\.<\/span>css<span class=\"o\">)<\/span><span class=\"nv\">$\/<\/span>,\n        use: <span class=\"o\">[<\/span>MiniCssExtractPlugin.loader, <span class=\"o\">{<\/span> loader: <span class=\"s1\">'css-loader'<\/span>, options: <span class=\"o\">{<\/span> url: <span class=\"nb\">false<\/span> <span class=\"o\">}<\/span> <span class=\"o\">}<\/span>, <span class=\"s1\">'postcss-loader'<\/span><span class=\"o\">]<\/span>,\n      <span class=\"o\">}<\/span>,\n      <span class=\"o\">{<\/span>\n        <span class=\"nb\">test<\/span>: \/<span class=\"se\">\\.<\/span>js<span class=\"nv\">$\/<\/span>,\n        loader: <span class=\"s1\">'babel-loader'<\/span>,\n        options: <span class=\"o\">{<\/span>\n          exclude: \/node_modules\/,\n          plugins: <span class=\"o\">[<\/span><span class=\"s1\">'@babel\/plugin-syntax-dynamic-import'<\/span>, <span class=\"s1\">'@babel\/transform-object-assign'<\/span><span class=\"o\">]<\/span>,\n          cacheDirectory: <span class=\"nb\">true<\/span>,\n          presets: <span class=\"o\">[<\/span>\n            <span class=\"o\">[<\/span>\n              <span class=\"s1\">'@babel\/preset-env'<\/span>\n            <span class=\"o\">]<\/span>\n          <span class=\"o\">]<\/span>\n        <span class=\"o\">}<\/span>,\n      <span class=\"o\">}<\/span>,\n    <span class=\"o\">]<\/span>,\n  <span class=\"o\">}<\/span>,\n  plugins: <span class=\"o\">[<\/span>\n    new MiniCssExtractPlugin<span class=\"o\">({<\/span>\n      filename: <span class=\"s1\">'[name].css'<\/span>,\n      chunkFilename: <span class=\"s1\">'[name].[chunkhash:3].css'<\/span>,\n    <span class=\"o\">})<\/span>,\n    new WebpackRequireFrom<span class=\"o\">({<\/span>\n      variableName: <span class=\"s1\">'window.__CONTEXT__.cdnUrl'<\/span>,\n    <span class=\"o\">})<\/span>,\n    new webpack.DefinePlugin<span class=\"o\">({<\/span>\n      <span class=\"s1\">'process.env'<\/span>: <span class=\"o\">{<\/span>\n        NODE_ENV: JSON.stringify<span class=\"o\">(<\/span>process.env.NODE_ENV<span class=\"o\">)<\/span>,\n      <span class=\"o\">}<\/span>,\n    <span class=\"o\">})<\/span>,\n  <span class=\"o\">]<\/span>,\n  mode: production ? <span class=\"s1\">'production'<\/span> : <span class=\"s1\">'development'<\/span>,\n<span class=\"o\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h3>\n  \n  \n  Note about static assets\n<\/h3>\n\n<p>We do not process static assets with webpack. Fonts, images are just sitting already optimized in their proper place in our platformOS assets directory - app\/assets. There are a couple of reasons we do that:<\/p>\n\n<ol>\n<li>Manual optimization is usually better than automatic<\/li>\n<li>Config is simpler<\/li>\n<li>Fewer dependencies to break<\/li>\n<li>Build is faster<\/li>\n<li>We don't waste CPU time (= money) on CI doing the same operation on every build<\/li>\n<\/ol>\n\n<h2>\n  \n  \n  Technologies\n<\/h2>\n\n<p><strong><a href=\"https:\/\/postcss.org\/\">PostCSS<\/a><\/strong>\u00a0is a framework for creating CSS parsing plugins. It allowed us to migrate away from SASS to reduce complexity. It handles some legacy browser fixes. It is also a base for TailwindCSS, a PostCSS plugin.<\/p>\n\n<p><strong><a href=\"http:\/\/tailwindcss.com\/\">TailwindCSS<\/a><\/strong>\u00a0is a CSS framework that makes it easy to create maintainable views with a minimal CSS footprint.<\/p>\n\n<p>If you are interested in how we set up TailwindCSS and PostCSS, visit\u00a0<a href=\"https:\/\/github.com\/mdyd-dev\/platformos-documentation\/\">our documentation GitHub repository<\/a>.<\/p>\n\n<h2>\n  \n  \n  Base config\n<\/h2>\n\n<p>Out of the box, webpack can work with no config at all.\u00a0<a href=\"https:\/\/auth0.com\/blog\/zero-config-javascript-app-prototyping-with-webpack\/\">Read more on prototyping with no config<\/a>. By default, it looks for the <code>index.js<\/code> file in the <code>src\/<\/code> directory and outputs to <code>dist\/main.js<\/code>. We need to use a couple of features, so we need a config file. The webpack settings we use are pretty simple. <\/p>\n\n<h3>\n  \n  \n  Entry points\n<\/h3>\n\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>entry: <span class=\"o\">{<\/span>\n  <span class=\"s1\">'app'<\/span>: <span class=\"s1\">'.\/src\/app'<\/span>,\n  <span class=\"s1\">'graphql'<\/span>: <span class=\"s1\">'.\/modules\/graphql\/public\/assets\/graphql'<\/span>,\n<span class=\"o\">}<\/span>,\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>We use two entry points (files that we will pull in from our HTML) because we have completely separate CSS files for our documentation page and autogenerated documentation for GraphQL endpoints. <code>key<\/code> is to define how the output will be named, <code>value<\/code> is the relative path to the file webpack will treat as an entry point. For example: <code>app.js<\/code> will be generated from entry point <code>src\/app.js<\/code>.<\/p>\n\n<h3>\n  \n  \n  Output\n<\/h3>\n\n<p>We don't define the output filename because by default, it is <code>[name].js<\/code>, and it works for our case. But for <code>chunkFilename<\/code>, we need to add chunkhash for cache invalidation purposes.<\/p>\n\n<p><code>publicPath<\/code> is set to empty because we will not use webpack's built in publicPath \u2014 we will use the one from <code>WebpackRequireFrom<\/code>, explained later.<\/p>\n\n<p>path is the absolute path where the output files will be placed. In the case of platformOS, it is <code>app\/assets<\/code>.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>output: <span class=\"o\">{<\/span>\n    chunkFilename: <span class=\"s1\">'[name].[chunkhash:3].js'<\/span>,\n    publicPath: <span class=\"s1\">''<\/span>,\n    path: path.resolve<span class=\"o\">(<\/span><span class=\"s1\">'app\/assets'<\/span><span class=\"o\">)<\/span>,\n  <span class=\"o\">}<\/span>,\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h2>\n  \n  \n  Plugins\n<\/h2>\n\n<p><strong><a href=\"https:\/\/github.com\/webpack-contrib\/mini-css-extract-plugin\">MiniCssExtractPlugin<\/a><\/strong> extracts CSS to external files. Without it, webpack would package everything into one JS and one CSS.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>new MiniCssExtractPlugin<span class=\"o\">({<\/span>\n    filename: <span class=\"s1\">'[name].css'<\/span>,\n    chunkFilename: <span class=\"s1\">'[name].[chunkhash:3].css'<\/span>,\n<span class=\"o\">})<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>The platformOS Liquid filter for getting the URL to assets placed on the CDN adds the query param to bust the cache, so the main files that we require in Liquid do not need to vary in between different builds. We add <code>chunkhash:3<\/code> in the file name for chunks because those are loaded dynamically from webpack, and there won't be a query param to handle that.<\/p>\n\n<p>Why all that? To prevent browsers from loading a file from cache (old asset could break something) if it changed.<\/p>\n\n<p>Files loaded using the native platformOS <code>asset_url<\/code> filter:<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--ygtcn7MN--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/8nr7km8tm2h6m0g2xnc6.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--ygtcn7MN--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/8nr7km8tm2h6m0g2xnc6.png\" alt=\"asset_url cache bust\" width=\"646\" height=\"152\"><\/a><\/p>\n\n<p>Chunks loaded with chunkhash in the filename:<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--wPNpxrIF--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/1l265an5z4adyryqxnlp.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--wPNpxrIF--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/1l265an5z4adyryqxnlp.png\" alt=\"chunks filename cache bust\" width=\"348\" height=\"104\"><\/a><\/p>\n\n<p><strong><a href=\"https:\/\/github.com\/agoldis\/webpack-require-from\">WebpackRequireFrom<\/a><\/strong> sets the CDN URL during runtime for dynamic chunks, so we don't have to hardcode anything. We just pull this URL from the HTML source, which is rendered server-side.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>new WebpackRequireFrom<span class=\"o\">({<\/span>\n    variableName: <span class=\"s1\">'window.__CONTEXT__.cdnUrl'<\/span>,\n<span class=\"o\">})<\/span>,\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This option tells webpack where to look to get the CDN URL in the browser when the application is running. In Liquid, we define <code>window.__CONTEXT__.cdnUrl<\/code> to our CDN URL to let webpack know where to look for dynamically loaded chunks.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>&lt;script&gt;\n  window.__CONTEXT__ <span class=\"o\">=<\/span> <span class=\"o\">{<\/span> cdnUrl: <span class=\"s2\">\"{{ '' | asset_url }}\"<\/span> <span class=\"o\">}<\/span><span class=\"p\">;<\/span>\n&lt;\/script&gt;\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>This results in chunks loaded correctly on all environments provided by platformOS: dev, staging, and production.<\/p>\n\n<p>The <strong><a href=\"https:\/\/webpack.js.org\/plugins\/define-plugin\/\">webpack.DefinePlugin<\/a><\/strong> allows us to pass build mode (production or dev) to the runtime. This is needed by the Algolia search script that we use as our search engine.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>new webpack.DefinePlugin<span class=\"o\">({<\/span>\n <span class=\"s1\">'process.env'<\/span>: <span class=\"o\">{<\/span>\n     NODE_ENV: JSON.stringify<span class=\"o\">(<\/span>process.env.NODE_ENV<span class=\"o\">)<\/span>,\n  <span class=\"o\">}<\/span>,\n<span class=\"o\">})<\/span>,\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>Keep in mind that for this line to work, we execute webpack-cli with NODE_ENV defined in the npm script.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"s2\">\"build\"<\/span>: <span class=\"s2\">\"NODE_ENV=production npx webpack-cli --no-color\"<\/span>,\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<h2>\n  \n  \n  Loaders\n<\/h2>\n\n<p>Loaders are specific plugins for webpack that extend its functionality to operate on files that it does not understand by default. By default, webpack can only parse and bundle JavaScript.<\/p>\n\n<p>The order of loaders matters. For example, css-loader cannot parse syntax written for PostCSS, so PostCSS needs to be executed first. Loaders are executed from right to left.<\/p>\n\n<p>postcss-loader allows webpack to use the PostCSS plugins ecosystem. It usually needs the postcss.config.js file to add PostCSS plugins because PostCSS on its own doesn't do anything.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"o\">{<\/span>\n    <span class=\"nb\">test<\/span>: \/<span class=\"o\">(<\/span><span class=\"se\">\\.<\/span>css<span class=\"o\">)<\/span><span class=\"nv\">$\/<\/span>,\n    use: <span class=\"o\">[<\/span>\n            MiniCssExtractPlugin.loader,\n            <span class=\"o\">{<\/span> loader: <span class=\"s1\">'css-loader'<\/span>, options: <span class=\"o\">{<\/span> url: <span class=\"nb\">false<\/span> <span class=\"o\">}<\/span> <span class=\"o\">}<\/span>,\n            <span class=\"s1\">'postcss-loader'<\/span>\n        <span class=\"o\">]<\/span>,\n<span class=\"o\">}<\/span>,\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<p>For CSS files, PostCSS will do its thing first, pass the output to css-loader, it will do its thing (without resolving URLs in CSS) and pass the output to MiniCssExtractPlugin.loader which will save the CSS to separate files.<\/p>\n\n<p><a href=\"https:\/\/github.com\/babel\/babel-loader\">babel-loader<\/a> transpiles JavaScript using babel and its plugins. We use <code>preset-env<\/code> and two plugins: <code>syntax-dynamic-import<\/code> and <code>transform-object-assign<\/code> for browsers defined in <a href=\"https:\/\/github.com\/mdyd-dev\/platformos-documentation\/blob\/1505cf2c7ace239d46abec979f2c01f0b4d5bd1b\/package.json#L27\">package.json<\/a>.<\/p>\n\n<h2>\n  \n  \n  Code splitting and conditional loading\n<\/h2>\n\n<p>This article wouldn't be complete if I did not include how to split code in webpack build time so that it asynchronously loads chunks while running in the browser.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>import <span class=\"s1\">'.\/app.css'<\/span><span class=\"p\">;<\/span>\n\nimport <span class=\"o\">{<\/span> <span class=\"nv\">$q<\/span> <span class=\"o\">}<\/span> from <span class=\"s1\">'.\/js\/helpers\/dom'<\/span><span class=\"p\">;<\/span>\n\nimport <span class=\"s1\">'.\/js\/sidebarMenu'<\/span><span class=\"p\">;<\/span>\nimport <span class=\"s1\">'.\/js\/deepLinks'<\/span><span class=\"p\">;<\/span>\nimport <span class=\"s1\">'.\/js\/autosteps'<\/span><span class=\"p\">;<\/span> \/\/ this HAS to be after deepLinks\nimport <span class=\"s1\">'.\/js\/toc'<\/span><span class=\"p\">;<\/span>\nimport <span class=\"s1\">'.\/js\/externalLinks'<\/span><span class=\"p\">;<\/span>\nimport <span class=\"s1\">'.\/js\/feedback'<\/span><span class=\"p\">;<\/span>\n\nimport<span class=\"o\">(<\/span>\/<span class=\"k\">*<\/span> webpackChunkName: <span class=\"s2\">\"search\"<\/span> <span class=\"k\">*<\/span>\/ <span class=\"s1\">'.\/js\/search'<\/span><span class=\"o\">)<\/span><span class=\"p\">;<\/span>\n\n<span class=\"k\">if<\/span> <span class=\"o\">(<\/span><span class=\"nv\">$q<\/span><span class=\"o\">(<\/span><span class=\"s1\">'code[class*=\"language-\"]'<\/span><span class=\"o\">))<\/span> <span class=\"o\">{<\/span>\n  import<span class=\"o\">(<\/span>\/<span class=\"k\">*<\/span> webpackChunkName: <span class=\"s2\">\"syntaxHighlighting\"<\/span> <span class=\"k\">*<\/span>\/ <span class=\"s1\">'.\/js\/syntaxHighlighting'<\/span><span class=\"o\">)<\/span><span class=\"p\">;<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n\n<ul>\n<li>All the ES6 imports (from <code>sidebarMenu<\/code> to <code>feedback<\/code>) will be bundled directly into app.js.<\/li>\n<li>Dynamic imports (function named import - <code>search<\/code>) will generate chunks, that will be loaded asynchronously.<\/li>\n<li>Dynamic imports wrapped in an <code>if<\/code> statement will be loaded asynchronously only when the <code>if<\/code> condition evaluates to a <code>truthy<\/code> value. In our case, when a certain selector is present on the webpage. Webpack will not load those modules on pages that do not require them.<\/li>\n<\/ul>\n\n<p>Example requests for the homepage:<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--fc297_QN--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/trfr3c1kt07u7jtb7wla.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--fc297_QN--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/trfr3c1kt07u7jtb7wla.png\" alt=\"Conditional loading - no syntax highlighting\" width=\"500\" height=\"308\"><\/a><\/p>\n\n<p>Example requests for a page where syntax highlighting is needed:<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--aBT_6nqh--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/fhabisiigwglhyq3nr87.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--aBT_6nqh--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/fhabisiigwglhyq3nr87.png\" alt=\"Conditional loading - with syntax highlighting\" width=\"500\" height=\"394\"><\/a><\/p>\n\n<h3>\n  \n  \n  Closing thoughts\n<\/h3>\n\n<p>Webpack in 2020 is a totally different beast than it was in 2015, but it can still be intimidating. If you don't feel its syntax, you can try different code bundlers; some of them are simpler to some people (<a href=\"https:\/\/www.rollupjs.org\/\">rollup<\/a>), and some are much faster (<a href=\"https:\/\/esbuild.github.io\/\">esbuild<\/a>). We chose webpack a long time ago because it was reliable, the most feature-rich (thanks to plugins), powerful, and very well documented.<\/p>\n\n<p>If you are interested in a Webpack + TailwindCSS setup for static websites (with HTML that you can deploy to platformOS, netlify, vercel, or just AWS S3), we created an <a href=\"https:\/\/github.com\/pavelloz\/webpack-tailwindcss-purgecss\">opensource boilerplate<\/a> for you to quickly jump in and test it out.<\/p>\n\n","category":["webperf","webpack","platformos"]},{"title":"How to Test Slack Notifications","pubDate":"Thu, 26 Nov 2020 17:25:29 +0000","link":"https:\/\/dev.to\/platformos\/how-to-test-slack-notifications-2leb","guid":"https:\/\/dev.to\/platformos\/how-to-test-slack-notifications-2leb","description":"<p>Marketing forms are often connected to a third-party system to help the marketing team provide faster and more accurate responses.<\/p>\n\n<p>We integrated our contact page with Slack, HubSpot, SendGrid, and emails. Whenever someone fills in the form, we notify the appropriate people via email, Slack message, and create records in HubSpot and SendGrid for future management of those contacts.<\/p>\n\n<p><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%2Fi%2F9qyvtud4azy2jbos1fpq.png\" class=\"article-body-image-wrapper\"><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%2Fi%2F9qyvtud4azy2jbos1fpq.png\" alt=\"Contact form\"><\/a><\/p>\n\n<p>One day we received the email, but the Slack notification did not come through, and this made me think about testing Slack notifications to catch regressions in this area.<\/p>\n\n<p>To achieve this, we need two things:<\/p>\n\n<ul>\n<li>A test channel for test messages<\/li>\n<li>A test that will send the form for us<\/li>\n<\/ul>\n\n<h3>\n  \n  \n  platformOS notification setup\n<\/h3>\n\n<p>platformOS API Call notifications support WebHooks, so that's what we used for Slack messages. The setup involves only a couple of lines of code, but the most important in the context of this article is the WebHook URL.<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"nn\">---<\/span>\n<span class=\"na\">name<\/span><span class=\"pi\">:<\/span> <span class=\"s\">slack_landing_sales<\/span>\n<span class=\"na\">delay<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">0'<\/span>\n<span class=\"na\">enabled<\/span><span class=\"pi\">:<\/span> <span class=\"kc\">true<\/span>\n<span class=\"na\">format<\/span><span class=\"pi\">:<\/span> <span class=\"s\">http<\/span>\n<span class=\"na\">headers<\/span><span class=\"pi\">:<\/span> <span class=\"s1\">'<\/span><span class=\"s\">{<\/span><span class=\"nv\"> <\/span><span class=\"s\">\"Content-Type\":<\/span><span class=\"nv\"> <\/span><span class=\"s\">\"application\/json\"<\/span><span class=\"nv\"> <\/span><span class=\"s\">}'<\/span>\n<span class=\"na\">request_type<\/span><span class=\"pi\">:<\/span> <span class=\"s\">POST<\/span>\n<span class=\"na\">to<\/span><span class=\"pi\">:<\/span> <span class=\"s\">https:\/\/hooks.slack.com\/services\/XXX\/XXX\/XXX<\/span>\n<span class=\"na\">trigger_condition<\/span><span class=\"pi\">:<\/span> <span class=\"kc\">true<\/span>\n<span class=\"nn\">---<\/span>\n<span class=\"pi\">{<\/span>\n<span class=\"s2\">\"<\/span><span class=\"s\">text\"<\/span><span class=\"pi\">:<\/span> <span class=\"s2\">\"<\/span>\n<span class=\"s\">Heads<\/span><span class=\"nv\"> <\/span><span class=\"s\">up!<\/span><span class=\"nv\"> <\/span><span class=\"s\">Contact<\/span><span class=\"nv\"> <\/span><span class=\"s\">from<\/span><span class=\"nv\"> <\/span><span class=\"s\">{{<\/span><span class=\"nv\"> <\/span><span class=\"s\">form.properties.url<\/span><span class=\"nv\"> <\/span><span class=\"s\">}}.<\/span>\n<span class=\"s\">Name:<\/span><span class=\"nv\"> <\/span><span class=\"s\">{{<\/span><span class=\"nv\"> <\/span><span class=\"s\">form.properties.first_name<\/span><span class=\"nv\"> <\/span><span class=\"s\">}}<\/span><span class=\"nv\"> <\/span><span class=\"s\">{{<\/span><span class=\"nv\"> <\/span><span class=\"s\">form.properties.last_name<\/span><span class=\"nv\"> <\/span><span class=\"s\">}}<\/span>\n<span class=\"s\">Email:<\/span><span class=\"nv\"> <\/span><span class=\"s\">{{<\/span><span class=\"nv\"> <\/span><span class=\"s\">form.properties.email<\/span><span class=\"nv\"> <\/span><span class=\"s\">}}<\/span>\n<span class=\"s\">Phone:<\/span><span class=\"nv\"> <\/span><span class=\"s\">{{<\/span><span class=\"nv\"> <\/span><span class=\"s\">form.properties.phone<\/span><span class=\"nv\"> <\/span><span class=\"s\">}}<\/span>\n<span class=\"s\">Comments:<\/span><span class=\"nv\"> <\/span><span class=\"s\">{{<\/span><span class=\"nv\"> <\/span><span class=\"s\">form.properties.description<\/span><span class=\"nv\"> <\/span><span class=\"s\">}}\"<\/span><span class=\"pi\">,<\/span>\n<span class=\"pi\">}<\/span> \n<\/code><\/pre>\n\n<\/div>\n\n\n<p>The line to change is the one with <code>to: [https:\/\/hooks.slack.com\/services\/XXX\/XXX\/XXX](https:\/\/hooks.slack.com\/services\/XXX\/XXX\/XXX)<\/code> which I censored. You need your own webhook URL to be able to send messages to your channel.<\/p>\n\n<p>To not send test data to the production Slack channel, it is a good idea to differentiate production from everything else:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"na\">to<\/span><span class=\"pi\">:<\/span> <span class=\"pi\">&gt;<\/span>\n  <span class=\"s\">{% if form.properties.url contains 'https:\/\/www.platformos.com' %}<\/span>\n  <span class=\"s\">https:\/\/hooks.slack.com\/services\/XXX\/XXX\/XXX<\/span>\n  <span class=\"s\">{% else %}<\/span>\n  <span class=\"s\">https:\/\/hooks.slack.com\/services\/YYY\/YYY\/YYY<\/span>\n  <span class=\"s\">{% endif %}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>This if condition will check if the form field called <code>url<\/code> contains a particular string. Because we are populating it before sending the form, it is filled in with the URL of the instance. In the case of our production site, it is <code>https:\/\/www.platformos.com<\/code>. <\/p>\n<h3>\n  \n  \n  E2E test in TestCafe\n<\/h3>\n\n<p>Now, we need to programmatically go through the form, fill it, send it, and validate that it has been sent successfully.<\/p>\n\n<p>We use TestCafe for E2E tests. It is not too difficult to start using it if you have experience with testing.<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight jsx\"><code><span class=\"k\">import<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">Selector<\/span> <span class=\"p\">}<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">testcafe<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n<span class=\"k\">import<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">BASE_URL<\/span> <span class=\"p\">}<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">.\/env<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n<span class=\"k\">import<\/span> <span class=\"nx\">faker<\/span> <span class=\"k\">from<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">faker<\/span><span class=\"dl\">'<\/span><span class=\"p\">;<\/span>\n\n<span class=\"nf\">fixture<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Contact<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nf\">page<\/span><span class=\"p\">(<\/span><span class=\"s2\">`<\/span><span class=\"p\">${<\/span><span class=\"nx\">BASE_URL<\/span><span class=\"p\">}<\/span><span class=\"s2\">\/contact`<\/span><span class=\"p\">);<\/span>\n\n<span class=\"nf\">test<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Contact form works<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"k\">async <\/span><span class=\"p\">(<\/span><span class=\"nx\">t<\/span><span class=\"p\">)<\/span> <span class=\"o\">=&gt;<\/span> <span class=\"p\">{<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">fn<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">[name=\"form[properties_attributes][first_name]\"]<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">ln<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">[name=\"form[properties_attributes][last_name]\"]<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">email<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">[name=\"form[properties_attributes][email]\"]<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">phone<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">[name=\"form[properties_attributes][phone]\"]<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">description<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">[name=\"form[properties_attributes][description]\"]<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n  <span class=\"kd\">const<\/span> <span class=\"nx\">submitBtn<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">button<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nf\">withText<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">SUBMIT<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"kd\">const<\/span> <span class=\"nx\">notice<\/span> <span class=\"o\">=<\/span> <span class=\"nc\">Selector<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">[role=\"alert\"]<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nf\">withText<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">Your contact form has been sent<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"k\">await<\/span> <span class=\"nx\">t<\/span>\n    <span class=\"p\">.<\/span><span class=\"nf\">typeText<\/span><span class=\"p\">(<\/span><span class=\"nx\">fn<\/span><span class=\"p\">,<\/span> <span class=\"nx\">faker<\/span><span class=\"p\">.<\/span><span class=\"nx\">name<\/span><span class=\"p\">.<\/span><span class=\"nf\">firstName<\/span><span class=\"p\">())<\/span>\n    <span class=\"p\">.<\/span><span class=\"nf\">typeText<\/span><span class=\"p\">(<\/span><span class=\"nx\">ln<\/span><span class=\"p\">,<\/span> <span class=\"nx\">faker<\/span><span class=\"p\">.<\/span><span class=\"nx\">name<\/span><span class=\"p\">.<\/span><span class=\"nf\">lastName<\/span><span class=\"p\">())<\/span>\n    <span class=\"p\">.<\/span><span class=\"nf\">typeText<\/span><span class=\"p\">(<\/span><span class=\"nx\">email<\/span><span class=\"p\">,<\/span> <span class=\"nx\">faker<\/span><span class=\"p\">.<\/span><span class=\"nx\">internet<\/span><span class=\"p\">.<\/span><span class=\"nf\">email<\/span><span class=\"p\">())<\/span>\n    <span class=\"p\">.<\/span><span class=\"nf\">typeText<\/span><span class=\"p\">(<\/span><span class=\"nx\">phone<\/span><span class=\"p\">,<\/span> <span class=\"nx\">faker<\/span><span class=\"p\">.<\/span><span class=\"nx\">phone<\/span><span class=\"p\">.<\/span><span class=\"nf\">phoneNumber<\/span><span class=\"p\">())<\/span>\n    <span class=\"p\">.<\/span><span class=\"nf\">typeText<\/span><span class=\"p\">(<\/span><span class=\"nx\">description<\/span><span class=\"p\">,<\/span> <span class=\"nx\">faker<\/span><span class=\"p\">.<\/span><span class=\"nx\">lorem<\/span><span class=\"p\">.<\/span><span class=\"nf\">paragraph<\/span><span class=\"p\">(),<\/span> <span class=\"p\">{<\/span> <span class=\"na\">paste<\/span><span class=\"p\">:<\/span> <span class=\"kc\">true<\/span> <span class=\"p\">});<\/span>\n\n  <span class=\"k\">await<\/span> <span class=\"nx\">t<\/span><span class=\"p\">.<\/span><span class=\"nf\">click<\/span><span class=\"p\">(<\/span><span class=\"nx\">submitBtn<\/span><span class=\"p\">);<\/span>\n\n  <span class=\"k\">await<\/span> <span class=\"nx\">t<\/span><span class=\"p\">.<\/span><span class=\"nf\">expect<\/span><span class=\"p\">(<\/span><span class=\"nx\">notice<\/span><span class=\"p\">.<\/span><span class=\"nx\">exists<\/span><span class=\"p\">).<\/span><span class=\"nf\">ok<\/span><span class=\"p\">();<\/span>\n<span class=\"p\">});<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>A short summary of what is going on here:<\/p>\n\n<ol>\n<li>Importing TestCafe and faker (generates fake data so that the test varies every time it is run)<\/li>\n<li>Telling TestCafe the URL at which the test will be run <\/li>\n<li>Defining selectors for all the DOM elements we will be interacting with<\/li>\n<li>Filling in all the data in the form<\/li>\n<li>Submitting the form<\/li>\n<li>Checking if the <code>Your contact form has been sent<\/code> message is visible after the form has been sent<\/li>\n<\/ol>\n\n<p>Running the test in chromium results in a pretty video:<\/p>\n\n<p><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%2Fi%2Fctr2y7b51yuc85fx6swl.gif\" class=\"article-body-image-wrapper\"><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%2Fi%2Fctr2y7b51yuc85fx6swl.gif\" alt=\"TestCafe running\"><\/a><\/p>\n\n<p>In your terminal, you should see something similar after running TestCafe:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"o\">&gt;<\/span> testcafe chromium tests <span class=\"nt\">--video<\/span> artifacts <span class=\"nt\">--video-encoding-options<\/span> <span class=\"nv\">r<\/span><span class=\"o\">=<\/span>20\n\n Running tests <span class=\"k\">in<\/span>:\n - Chrome 88.0.4295.0 \/ macOS 10.15.7\n\n Contact\n \u2713 Contact form works\n\n 1 passed <span class=\"o\">(<\/span>38s<span class=\"o\">)<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>This means that the test passed, so now it is time to verify if the Slack notification came through. This is what this test generated:<\/p>\n\n<p><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%2Fi%2Fckfj1lgyidt6s0dmi982.png\" class=\"article-body-image-wrapper\"><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%2Fi%2Fckfj1lgyidt6s0dmi982.png\" alt=\"Slack message\"><\/a><\/p>\n<h3>\n  \n  \n  Continuous Integration\n<\/h3>\n\n<p>To make our lives even more automated, we add the <code>npm run test-ci<\/code> command to our CI workflow. For this project, we use Jenkins, but you might use Github Actions, Travis, or CircleCI if you prefer. Remember that your CI server has to have a browser supported by TestCafe (<a href=\"https:\/\/devexpress.github.io\/testcafe\/documentation\/guides\/concepts\/browsers.html#officially-supported-browsers\" rel=\"noopener noreferrer\">see the list of officially supporter browsers here<\/a>).<\/p>\n\n<p>In our case, making tests run on every master branch run was similar to:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>stage<span class=\"o\">(<\/span><span class=\"s1\">'Test on URL'<\/span><span class=\"o\">)<\/span> <span class=\"o\">{<\/span>\n    agent <span class=\"o\">{<\/span> docker <span class=\"o\">{<\/span> image <span class=\"s2\">\"platformos\/testcafe\"<\/span> <span class=\"o\">}<\/span> <span class=\"o\">}<\/span>\n    environment <span class=\"o\">{<\/span> MP_URL <span class=\"o\">=<\/span> <span class=\"s2\">\"<\/span><span class=\"k\">${<\/span><span class=\"nv\">params<\/span><span class=\"p\">.MP_URL<\/span><span class=\"k\">}<\/span><span class=\"s2\">\"<\/span> <span class=\"o\">}<\/span>\n    steps <span class=\"o\">{<\/span>\n      sh <span class=\"s1\">'npm run test-ci'<\/span>\n    <span class=\"o\">}<\/span>\n    post <span class=\"o\">{<\/span> failure <span class=\"o\">{<\/span> archiveArtifacts <span class=\"s2\">\"screenshots\/\"<\/span> <span class=\"o\">}<\/span> <span class=\"o\">}<\/span>\n<span class=\"o\">}<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>We also use a different command on CI because we want to take screenshots of failures only and run the test in a headless browser (it is much faster \u2014 the test above runs in 6 seconds instead of 38):<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>testcafe <span class=\"s1\">'chromium:headless'<\/span> <span class=\"nt\">--screenshots-on-fails<\/span> <span class=\"nt\">--screenshots<\/span><span class=\"o\">=<\/span>screenshots tests\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Now every time any code is merged into the master branch, TestCafe will test this form and trigger the Slack webhook. If the message will be missing one day, or if the test fails, we will know something is wrong either on our side or Slack's.<\/p>\n<h2>\n  \n  \n  Resources\n<\/h2>\n\n<p>I hope this guide will help you avoid some of the regressions. Here are some resources that you might find useful if you decide to follow our path:<\/p>\n\n<p><a href=\"https:\/\/www.platformos.com\" rel=\"noopener noreferrer\">platformOS Website<\/a><\/p>\n\n<p><a href=\"https:\/\/documentation.platformos.com\/\" rel=\"noopener noreferrer\">platformOS Documentation<\/a><\/p>\n\n<p><a href=\"https:\/\/slack.com\/intl\/en-it\/help\/articles\/115005265063-Incoming-webhooks-for-Slack\" rel=\"noopener noreferrer\">Slack - Creating App with WebHooks<\/a><\/p>\n\n<p><a href=\"https:\/\/devexpress.github.io\/testcafe\/\" rel=\"noopener noreferrer\">TestCafe testing framework<\/a><\/p>\n\n<p><a href=\"https:\/\/www.npmjs.com\/package\/faker\" rel=\"noopener noreferrer\">faker npm package<\/a><\/p>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/using-webp-in-your-existing-webpage-809\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Using WebP in Your Existing Webpage<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 17 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#design<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/optimizing-images-for-the-web-18gc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Optimizing Images For The Web<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Apr 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n        <span class=\"ltag__link__tag\">#node<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":"testing"},{"title":"Using WebP in Your Existing Webpage","pubDate":"Tue, 17 Nov 2020 14:44:16 +0000","link":"https:\/\/dev.to\/platformos\/using-webp-in-your-existing-webpage-809","guid":"https:\/\/dev.to\/platformos\/using-webp-in-your-existing-webpage-809","description":"<p>If you are checking your website's performance using Lighthouse, you might have noticed that one recommendation is to use \"<em>Serve images in next-gen formats<\/em>\" and by that, they mean WebP.<\/p>\n\n<p>According to Google, if you compress PNG images, you will save 26% on average.<\/p>\n\n<p>Compared to JPEG with a similar SSIM setting, the difference is even more significant \u2014 25%-34% depending on image type. We tested those claims on our JPEG photos from a smartphone, read on to learn about the results. <\/p>\n\n<p><strong>Note<\/strong>: Be careful what type of image you compress with WebP because it has no progressive loading like JPEG, so your hero image in WebP might negatively impact First Contentful Paint. <\/p>\n\n\n<blockquote class=\"ltag__twitter-tweet\">\n      <div class=\"ltag__twitter-tweet__media\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--YZWEzxnU--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/pbs.twimg.com\/media\/EmFbAxXWkAAbQCB.png\" alt=\"unknown tweet media content\">\n      <\/div>\n\n  <div class=\"ltag__twitter-tweet__main\">\n    <div class=\"ltag__twitter-tweet__header\">\n      <img class=\"ltag__twitter-tweet__profile-image\" src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--AjHtxuRG--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/pbs.twimg.com\/profile_images\/1265585417849634816\/OqwdR83A_normal.jpg\" alt=\"Harry Roberts profile image\">\n      <div class=\"ltag__twitter-tweet__full-name\">\n        Harry Roberts\n      <\/div>\n      <div class=\"ltag__twitter-tweet__username\">\n        @csswizardry\n      <\/div>\n      <div class=\"ltag__twitter-tweet__twitter-logo\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--ir1kO05j--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev.to\/assets\/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg\" alt=\"twitter logo\">\n      <\/div>\n    <\/div>\n    <div class=\"ltag__twitter-tweet__body\">\n      Fascinating issue on a current client site where we reduced their masthead image weight by over 20% by switching it to WebP. It\u2019s now rendering almost 2\u00d7 later because WebP doesn\u2019t offer progressive rendering like the previous JPG did. \n    <\/div>\n    <div class=\"ltag__twitter-tweet__date\">\n      19:55 PM - 05 Nov 2020\n    <\/div>\n\n\n    <div class=\"ltag__twitter-tweet__actions\">\n      <a href=\"https:\/\/twitter.com\/intent\/tweet?in_reply_to=1324440103792578562\" class=\"ltag__twitter-tweet__actions__button\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--fFnoeFxk--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev.to\/assets\/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg\" alt=\"Twitter reply action\">\n      <\/a>\n      <a href=\"https:\/\/twitter.com\/intent\/retweet?tweet_id=1324440103792578562\" class=\"ltag__twitter-tweet__actions__button\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--k6dcrOn8--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev.to\/assets\/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg\" alt=\"Twitter retweet action\">\n      <\/a>\n      <a href=\"https:\/\/twitter.com\/intent\/like?tweet_id=1324440103792578562\" class=\"ltag__twitter-tweet__actions__button\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--SRQc9lOp--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev.to\/assets\/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg\" alt=\"Twitter like action\">\n      <\/a>\n    <\/div>\n  <\/div>\n<\/blockquote>\n\n\n<h2>\n  \n  \n  If WebP is so good, why isn't everyone using it?\n<\/h2>\n\n<p>In short: Tooling.<\/p>\n\n<ul>\n<li>Your phone takes photos in JPEG.<\/li>\n<li>Your computer takes screenshots as PNG or JPEG.<\/li>\n<li>When you download images from a stock photo service, it's JPEG.<\/li>\n<li>Logotypes are often SVG.<\/li>\n<li>Your operating system most likely does not know how to preview WebP natively, so you need to download special software to do it.<\/li>\n<\/ul>\n\n<p>All these are barriers to adoption for this format, which apart from having better compression, has an alpha channel, which is missing from JPEG.<\/p>\n\n<h2>\n  \n  \n  Our case\n<\/h2>\n\n<p>One day, looking at a performance report from our <a href=\"https:\/\/github.com\/mdyd-dev\/product-marketplace-template\">social commerce marketplace template<\/a> (see live demo at: <a href=\"https:\/\/getmarketplace.co\/\">https:\/\/getmarketplace.co\/<\/a> ), I decided to do something about it and add WebP versions of the biggest images.<\/p>\n\n<p>Why only the biggest images? Because the bigger the image, the bigger the savings.<\/p>\n\n<p>Saving 20% from a 9 KB lazy-loaded image is a good thing, but saving 20% from 3 images eagerly-loaded, 500 KB each, is much better.<\/p>\n\n<h3>\n  \n  \n  Browser compatibility\n<\/h3>\n\n<p>Not all browsers support WebP, so you'll need a fallback for those browsers. Luckily, the picture HTML tag takes care of that.<\/p>\n\n<p>Read about browser support for WebP at caniuse.com: <a href=\"https:\/\/caniuse.com\/?search=webp\">https:\/\/caniuse.com\/?search=webp<\/a>  <\/p>\n\n<p>In any case, you can use WebP nowadays with a fallback to JPEG - read how to do it (and more) in this <a href=\"https:\/\/css-tricks.com\/using-webp-images\/\">excellent CSS-Tricks article<\/a>.<\/p>\n\n<h3>\n  \n  \n  platformOS image versions\n<\/h3>\n\n<p>Users very rarely have images in an optimal format or version. Often users upload photos straight from their camera or photos in PNG format. More often than not, you want to recompress those images to fit your performance and quality vision. We implemented a feature that allows the site owner to decide what to do with user-uploaded images.<\/p>\n\n<p>After the user uploads the image\u00a0<strong>directly to cloud storage<\/strong>\u00a0to speed up things by eliminating intermediaries, a function takes the original file and generates new versions based on the configuration defined in a YML file. It usually takes so little time that the user will not notice when it happens, as it happens in the background, asynchronously.<\/p>\n\n<p>In our case, the basic configuration looked like this:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"na\">name<\/span><span class=\"pi\">:<\/span> <span class=\"s\">photo<\/span>\n<span class=\"na\">properties<\/span><span class=\"pi\">:<\/span>\n  <span class=\"pi\">-<\/span> <span class=\"na\">name<\/span><span class=\"pi\">:<\/span> <span class=\"s\">photo<\/span>\n    <span class=\"na\">type<\/span><span class=\"pi\">:<\/span> <span class=\"s\">upload<\/span>\n    <span class=\"na\">options<\/span><span class=\"pi\">:<\/span>\n      <span class=\"na\">versions<\/span><span class=\"pi\">:<\/span>\n        <span class=\"pi\">-<\/span> <span class=\"na\">name<\/span><span class=\"pi\">:<\/span> <span class=\"s\">uncropped<\/span>\n          <span class=\"na\">output<\/span><span class=\"pi\">:<\/span>\n            <span class=\"na\">format<\/span><span class=\"pi\">:<\/span> <span class=\"s\">jpeg<\/span>\n            <span class=\"na\">quality<\/span><span class=\"pi\">:<\/span> <span class=\"m\">80<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>This means whatever image the user uploads will be available to use in its original form and an additional JPEG file will be generated called <code>uncropped<\/code> with quality 80. We wanted to also have this image in WebP format, so I added the second version to the array:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight yaml\"><code><span class=\"pi\">-<\/span> <span class=\"na\">name<\/span><span class=\"pi\">:<\/span> <span class=\"s\">uncropped_webp<\/span>\n  <span class=\"na\">output<\/span><span class=\"pi\">:<\/span>\n    <span class=\"na\">format<\/span><span class=\"pi\">:<\/span> <span class=\"s\">webp<\/span>\n    <span class=\"na\">quality<\/span><span class=\"pi\">:<\/span> <span class=\"m\">70<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n<h3>\n  \n  \n  HTML and Liquid\n<\/h3>\n\n<p>To show the images we have to get objects from GraphQL and retrieve URLs to the generated versions:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight html\"><code><span class=\"nt\">&lt;picture&gt;<\/span>\n  <span class=\"nt\">&lt;source<\/span> <span class=\"na\">srcset=<\/span><span class=\"s\">\"{{ p.photo.versions.uncropped_webp }}\"<\/span> <span class=\"na\">type=<\/span><span class=\"s\">\"image\/webp\"<\/span> <span class=\"na\">alt=<\/span><span class=\"s\">\"{{ item.name }}\"<\/span><span class=\"nt\">&gt;<\/span>\n  <span class=\"nt\">&lt;source<\/span> <span class=\"na\">srcset=<\/span><span class=\"s\">\"{{ p.photo.versions.uncropped }}\"<\/span> <span class=\"na\">type=<\/span><span class=\"s\">\"image\/jpeg\"<\/span> <span class=\"na\">alt=<\/span><span class=\"s\">\"{{ item.name }}\"<\/span><span class=\"nt\">&gt;<\/span>\n  <span class=\"nt\">&lt;img<\/span> <span class=\"na\">src=<\/span><span class=\"s\">\"{{ p.photo.versions.uncropped }}\"<\/span> <span class=\"na\">alt=<\/span><span class=\"s\">\"{{ item.name }}\"<\/span><span class=\"nt\">&gt;<\/span>\n<span class=\"nt\">&lt;\/picture&gt;<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n<h3>\n  \n  \n  Results\n<\/h3>\n\n<p>I uploaded three photos straight from my smartphone:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"o\">[<\/span>3.5M]  IMG_20201105_144639.jpg\n<span class=\"o\">[<\/span>4.2M]  IMG_20201109_194144.jpg\n<span class=\"o\">[<\/span>3.2M]  IMG_20201115_001933.jpg\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>They are very big in terms of dimensions (4608x2592 px) and size, so I expected to see a lot smaller files at the end of this experiment.<\/p>\n\n<p>Recompression to quality 80 in JPEG gave pretty good results:<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"o\">[<\/span>1.5M]  uncropped_IMG_20201105_144639.jpg\n<span class=\"o\">[<\/span>1.9M]  uncropped_IMG_20201109_194144.jpg\n<span class=\"o\">[<\/span>1.3M]  uncropped_IMG_20201115_001933.jpg \n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Let's compare them to the WebP version (quality 70):<br>\n<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code><span class=\"o\">[<\/span>1.0M]  uncropped_webp_IMG_20201105_144639.webp\n<span class=\"o\">[<\/span>1.0M]  uncropped_webp_IMG_20201109_194144.webp\n<span class=\"o\">[<\/span>996K]  uncropped_webp_IMG_20201115_001933.webp\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>The images vary in many ways, including color distribution or the numbers of edges. Because results depend on image content, an average is most often used when describing compression results. The smallest difference was 23%, and the biggest was 48%. That's a huge variance. In total, those three images saved 1.7 MB.<\/p>\n<h3>\n  \n  \n  Summary\n<\/h3>\n\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight shell\"><code>\u251c\u2500\u2500 <span class=\"o\">[<\/span> 11M]  original\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 <span class=\"o\">[<\/span>3.5M]  IMG_20201105_144639.jpg\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 <span class=\"o\">[<\/span>4.2M]  IMG_20201109_194144.jpg\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 <span class=\"o\">[<\/span>3.2M]  IMG_20201115_001933.jpg\n\u251c\u2500\u2500 <span class=\"o\">[<\/span>4.7M]  processed-jpeg\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 <span class=\"o\">[<\/span>1.5M]  uncropped_IMG_20201105_144639.jpg\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 <span class=\"o\">[<\/span>1.9M]  uncropped_IMG_20201109_194144.jpg\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 <span class=\"o\">[<\/span>1.3M]  uncropped_IMG_20201115_001933.jpg\n\u2514\u2500\u2500 <span class=\"o\">[<\/span>3.0M]  processed-webp\n    \u251c\u2500\u2500 <span class=\"o\">[<\/span>1.0M]  uncropped_webp_IMG_20201105_144639.webp\n    \u251c\u2500\u2500 <span class=\"o\">[<\/span>1.0M]  uncropped_webp_IMG_20201109_194144.webp\n    \u2514\u2500\u2500 <span class=\"o\">[<\/span>996K]  uncropped_webp_IMG_20201115_001933.webp\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Saving 1.7 MB (or 36%) on three images without noticeable quality degradation is a success in my book. We could turn up the quality knob higher, and we'd still have plenty of headroom not to lose any detail at all. It is a good result for a couple of lines of code. Especially if the image you are optimizing is a crucial piece of content (in our case, an item photo) and not some stock photo.<\/p>\n\n<blockquote>\n<p>If you want to check out how the files are different visually, you can <a href=\"https:\/\/github.com\/pavelloz\/jpeg-webp\">view them on my GitHub<\/a>.<\/p>\n<\/blockquote>\n<h3>\n  \n  \n  Additional resources\n<\/h3>\n\n<ul>\n<li><a href=\"https:\/\/developers.google.com\/speed\/webp\/gallery1\">Lossy gallery by Google<\/a><\/li>\n<li><a href=\"https:\/\/developers.google.com\/speed\/webp\/gallery2\">Lossless and Alpha gallery by Google<\/a><\/li>\n<\/ul>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/should-you-always-care-about-your-website-size-2jcc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Should You Always Care about Your Website Size?<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 12 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webdev<\/span>\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/optimizing-images-for-the-web-18gc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Optimizing Images For The Web<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Apr 24 '20 \u30fb 8 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n        <span class=\"ltag__link__tag\">#node<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20 \u30fb 6 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":["webperf","design"]},{"title":"Should You Always Care about Your Website Size?","pubDate":"Thu, 12 Nov 2020 12:35:30 +0000","link":"https:\/\/dev.to\/platformos\/should-you-always-care-about-your-website-size-2jcc","guid":"https:\/\/dev.to\/platformos\/should-you-always-care-about-your-website-size-2jcc","description":"<p><strong>TLDR<\/strong>: If you reduce your critical path size from 90 to 60 KB, it's good, but not all that great from a networking perspective. But going from 42 to 40 (especially on slow connections) might be more prominent. Always look at the cheat sheet and try to get to the smaller round trip size. I made those numbers bold so you can see if you are close to any of them.<\/p>\n\n<p>Cheatsheet of round trip sizes (in KB):<\/p>\n\n<div class=\"table-wrapper-paragraph\"><table>\n<thead>\n<tr>\n<th>Round trip<\/th>\n<th>Window Size (KB)<\/th>\n<th>Total size (KB)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1<\/td>\n<td>14<\/td>\n<td><strong>14<\/strong><\/td>\n<\/tr>\n<tr>\n<td>2<\/td>\n<td>28<\/td>\n<td><strong>42<\/strong><\/td>\n<\/tr>\n<tr>\n<td>3<\/td>\n<td>56<\/td>\n<td><strong>98<\/strong><\/td>\n<\/tr>\n<tr>\n<td>4<\/td>\n<td>112<\/td>\n<td><strong>210<\/strong><\/td>\n<\/tr>\n<tr>\n<td>5<\/td>\n<td>224<\/td>\n<td><strong>434<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/div>\n\n<p>I hope you don't need more for your critical path, but if you do, don't worry about round trips yet. Check the end of this article and some of my other articles for some tips.<\/p>\n\n\n\n\n<h2>\n  \n  \n  Round trip rules\n<\/h2>\n\n<p>Recently I read a couple of useful articles on how TCP works and especially how it snowballs (or slow-starts) with small packages into bigger chunks of data over the life of a connection. It is a little known fact that if your webpage critical path (basically HTML + CSS) can fit into the first round trip within a TCP connection, it is the best-case scenario because the browser will not ask the server again for anything.<br>\nThis rarely happens as webpages often use CSS frameworks, inline JavaScript, or have a lot of text on them. But there is also the second-best thing. Return your webpage within two round trips or three.<\/p>\n\n<p>The first round-trip has a size of 14 KB.<br>\nThe second one is the double of that. Which is 28. So at the end of the second round trip, your webpage will be loaded if it's below 28 + 14 = 42 KB<br>\nAgain, the third round trip is double the previous one, which is 2 * 28 = 56. 56 + 42 = 98. For quick reference look at the cheat sheet at the top of the article.<\/p>\n<h2>\n  \n  \n  Our case\n<\/h2>\n\n<p>We adjusted the homepage of our documentation website to fit in the first round trip. It was around 18 KBs, but had a lot of inlined stuff, and was optimized pretty well (already had 100 in Lighthouse).<\/p>\n\n<p>To go below 14 KB, we had to un-inline some things (mostly SVG images, favicon, which was another experiment in the making), clean up some unused SVG symbols (for icons), and at the end of the day, we landed at 12.17KB. 5.29KB for HTML and 6.88KB for CSS (thanks to TailwindCSS+PurgeCSS).<\/p>\n<h2>\n  \n  \n  Did it work?\n<\/h2>\n\n<p>Well, it's hard to tell. SSL handshakes take more time than the actual transfer of the critical data, so it is not bad from the frontend point of view. <br>\nThis is the waterfall before we managed to get below 14 KB<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--KvDW6idl--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/jotbzw05diwp27099hxd.jpg\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--KvDW6idl--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/jotbzw05diwp27099hxd.jpg\" alt=\"Waterfall before\" width=\"880\" height=\"374\"><\/a><\/p>\n\n<p>And this is after going below 14 KB: (+ addition of preconnect in HTTP header to hopefully speed up connection initialization to our CDN):<\/p>\n\n<p><a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--b-bhGvyS--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/fzbtdkjnh75j9hz16bj6.jpg\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--b-bhGvyS--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/fzbtdkjnh75j9hz16bj6.jpg\" alt=\"Waterfall after\" width=\"880\" height=\"414\"><\/a><\/p>\n\n<p>Note: Tests were made on the 1.5Mbps setting of WebPageTest.<\/p>\n\n<p>After the tests, we discovered that SSL handshakes can be optimized, so this will be our target very soon to make global improvements. We detected that our SSL handshake data weighs around 6KB, and it might account for the 14 KB TCP packets. We need more investigation in that area because there might be more to gain.<\/p>\n\n<p>The nature of testing on live websites with real devices is that you can't really predict exact numbers. Network conditions on both sides change. Device load matters. Many things can vary, and I could run the test a couple more times to prove my point, but I prefer to prove a different point: Just because something is better, it does not mean you will be able to show it on a chart every time. This is real life, not a laboratory.<\/p>\n<h2>\n  \n  \n  Inlining CSS \u2014 an experiment\n<\/h2>\n\n<p>Just for the sake of testing, I inlined the main CSS on the homepage to see what kind of results this will give. In the critical path it removed one SSL handshake, so in theory, it should move everything left on the waterfall chart.<\/p>\n\n<p>I did it by populating the Liquid partial with CSS after assets are built. A couple of lines of JavaScript did the trick:<br>\n<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code><span class=\"kd\">const<\/span> <span class=\"p\">{<\/span> <span class=\"nx\">writeFileSync<\/span><span class=\"p\">,<\/span> <span class=\"nx\">readFileSync<\/span> <span class=\"p\">}<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">require<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">fs<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n\n<span class=\"kd\">const<\/span> <span class=\"nx\">css<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">readFileSync<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">..\/app\/assets\/app.css<\/span><span class=\"dl\">'<\/span><span class=\"p\">).<\/span><span class=\"nx\">toString<\/span><span class=\"p\">();<\/span>\n<span class=\"kd\">const<\/span> <span class=\"nx\">fileContent<\/span> <span class=\"o\">=<\/span> <span class=\"nx\">css<\/span><span class=\"p\">.<\/span><span class=\"nx\">replace<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">.\/fonts\/Gotham<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"dl\">'<\/span><span class=\"s1\">\/assets\/fonts\/Gotham<\/span><span class=\"dl\">'<\/span><span class=\"p\">);<\/span>\n<span class=\"nx\">writeFileSync<\/span><span class=\"p\">(<\/span><span class=\"dl\">'<\/span><span class=\"s1\">..\/app\/views\/partials\/layouts\/head\/inline-css.liquid<\/span><span class=\"dl\">'<\/span><span class=\"p\">,<\/span> <span class=\"nx\">fileContent<\/span><span class=\"p\">);<\/span> \n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Results with inlining:<br>\n<a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--mo-pcddd--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/ntnhn1ynhqniptsjs839.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--mo-pcddd--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/ntnhn1ynhqniptsjs839.png\" alt=\"Waterfall with inlining\" width=\"880\" height=\"98\"><\/a><br>\n<a href=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--U9CGf7jE--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/lbjsfxd9g1m4skzmc78q.png\" class=\"article-body-image-wrapper\"><img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--U9CGf7jE--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/dev-to-uploads.s3.amazonaws.com\/i\/lbjsfxd9g1m4skzmc78q.png\" alt=\"Request timing with inlining\" width=\"880\" height=\"88\"><\/a><\/p>\n\n<p>It did very well - HTML and CSS were loaded just after 722ms, within one round trip within the first request. For the time being, we are not going to use this technique in production because of the added complexity in the development process. One day we will find a better way of automating this process, and it will become a reality.<\/p>\n<h2>\n  \n  \n  Conclusion\n<\/h2>\n\n<p>Answering the question from the title:\u00a0<strong>Should you always care about your website size?<\/strong><\/p>\n\n<p>Yes. It's always better to have a smaller website than a bigger one. Even if you cannot get spectacular results today, remember that good results come from a lot of smaller changes. So go for it, one step at a time.<\/p>\n\n<p>And no. If you are reducing 100KB from your 2MB React app, don't stress about it. There are more effective ways of spending your time, for example:<\/p>\n\n<ul>\n<li>lazy load,<\/li>\n<li>code split and asynchronous loading,<\/li>\n<li>tree shake,<\/li>\n<li>better alternatives to heavy frameworks,<\/li>\n<li>migration of CSS to use TailwindCSS,<\/li>\n<li>minimize fonts and optimize their loading<\/li>\n<\/ul>\n\n<p>All of those have proven to be very effective in improving user experience while being relatively fast to implement \u2014 one step at a time.<\/p>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/using-webp-in-your-existing-webpage-809\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Using WebP in Your Existing Webpage<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 17 '20 \u30fb 4 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#design<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/optimizing-images-for-the-web-18gc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Optimizing Images For The Web<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Apr 24 '20 \u30fb 8 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n        <span class=\"ltag__link__tag\">#node<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l3LNsE6N--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--XQubx9HX--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/organization\/profile_image\/2088\/97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\" width=\"150\" height=\"150\">\n      <div class=\"ltag__link__user__pic\">\n        <img src=\"https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--l5wkmzn0--\/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880\/https:\/\/res.cloudinary.com\/practicaldev\/image\/fetch\/s--dz8eofez--\/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150\/https:\/\/dev-to-uploads.s3.amazonaws.com\/uploads\/user\/profile_image\/51061\/c37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\" width=\"150\" height=\"150\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20 \u30fb 6 min read<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n \n\n","category":["webperf","webdev","beginners"]},{"title":"Image Maps with SVG \u2014 Back to the Future","pubDate":"Wed, 11 Nov 2020 15:02:58 +0000","link":"https:\/\/dev.to\/platformos\/image-maps-with-svg-back-to-the-future-19j","guid":"https:\/\/dev.to\/platformos\/image-maps-with-svg-back-to-the-future-19j","description":"<p>If you've been in the field of front-end development long enough, you might remember layouts made out of tables, hover states made using inline JavaScript, visitor counters, and the AltaVista search engine. CSS was much more primitive during that time, SVG probably wasn't supported by any browser, and most people called JavaScript DHTML (Dynamic HTML).<\/p>\n\n<p>There is a tag in HTML called <code>map<\/code>. It was heavily used when web pages were often not cut to HTML and CSS, but just one image marked to link to other pages. It allows you to mark parts of the image and attach <code>href<\/code> (and <code>onclick<\/code> if that is what you need) to them, so when the user clicks on a particular area, it will behave just like a standard link.<\/p>\n\n<p><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTML\/Element\/map\" rel=\"noopener noreferrer\">Read about map on MDN.<\/a><\/p>\n\n<h2>\n  \n  \n  Problem\n<\/h2>\n\n<p>Recently, we refactored the <a href=\"https:\/\/www.platformos.com\/\" rel=\"noopener noreferrer\">platformOS.com<\/a> website. The <strong>Values<\/strong> page had an image with absolutely positioned labels linking to the appropriate content: <\/p>\n\n<p><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%2Fi%2Fmhzho7ps77jj7s4jeptn.jpg\" class=\"article-body-image-wrapper\"><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%2Fi%2Fmhzho7ps77jj7s4jeptn.jpg\" alt=\"Values map\"><\/a><\/p>\n\n<p>This was done with CSS <code>position: absolute<\/code> and media queries to set positions on different screens. I could leave it as is, but because the refactor included a rewrite of CSS to TailwindCSS (for both maintenance and performance reasons), I decided to do some research. <\/p>\n\n<h2>\n  \n  \n  Solution\n<\/h2>\n\n<p>First, I tried to do the image map mentioned earlier, but it didn't work inside SVG. Then I discovered that you could do links in SVG. Every element in SVG has to have its <code>x<\/code> and <code>y<\/code> coordinates, so <code>a<\/code> link has features similar to HTML image <code>map<\/code>.<\/p>\n\n<p>A link inside SVG looks very similar to an HTML link:<\/p>\n\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight html\"><code>\n\n<span class=\"nt\">&lt;a<\/span> <span class=\"na\">href=<\/span><span class=\"s\">\"https:\/\/www.platformos.com\"<\/span><span class=\"nt\">&gt;<\/span>\n    <span class=\"nt\">&lt;text<\/span> <span class=\"na\">x=<\/span><span class=\"s\">\"10\"<\/span> <span class=\"na\">y=<\/span><span class=\"s\">\"25\"<\/span><span class=\"nt\">&gt;<\/span>platformOS - Limitless development platform<span class=\"nt\">&lt;\/text&gt;<\/span>\n<span class=\"nt\">&lt;\/a&gt;<\/span>\n\n\n<\/code><\/pre>\n\n<\/div>\n<p>This gives you the possibility, for example, to embed a link into your logo SVG so you don't have to use links every time you paste it. I decided to use it to have our values inside one image and not have to deal with media queries to position labels on every breakpoint.<\/p>\n\n<p>Example:<\/p>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight xml\"><code>\n\n<span class=\"nt\">&lt;svg<\/span> <span class=\"na\">class=<\/span><span class=\"s\">\"w-full\"<\/span> <span class=\"na\">xmlns=<\/span><span class=\"s\">\"http:\/\/www.w3.org\/2000\/svg\"<\/span> <span class=\"na\">viewBox=<\/span><span class=\"s\">\"0 0 1355.94 410.97\"<\/span><span class=\"nt\">&gt;<\/span>\n  <span class=\"nt\">&lt;g<\/span> <span class=\"na\">id=<\/span><span class=\"s\">\"Performance\"<\/span><span class=\"nt\">&gt;<\/span>\n    <span class=\"nt\">&lt;a<\/span> <span class=\"na\">xlink:href=<\/span><span class=\"s\">\"#performance\"<\/span><span class=\"nt\">&gt;<\/span>\n      <span class=\"nt\">&lt;rect<\/span> <span class=\"na\">x=<\/span><span class=\"s\">\"598.2\"<\/span> <span class=\"na\">width=<\/span><span class=\"s\">\"167.65\"<\/span> <span class=\"na\">height=<\/span><span class=\"s\">\"26\"<\/span> <span class=\"na\">fill=<\/span><span class=\"s\">\"#d50037\"<\/span><span class=\"nt\">&gt;&lt;\/rect&gt;<\/span>\n      <span class=\"nt\">&lt;g&gt;<\/span>\n        <span class=\"c\">&lt;!-- Letters --&gt;<\/span>\n      <span class=\"nt\">&lt;\/g&gt;<\/span>\n    <span class=\"nt\">&lt;\/a&gt;<\/span>\n  <span class=\"nt\">&lt;\/g&gt;<\/span>\n<span class=\"nt\">&lt;\/svg&gt;<\/span>\n\n\n<\/code><\/pre>\n\n<\/div>\n<p>Inspect this page for the full version of the SVG (it is inlined): <a href=\"https:\/\/www.platformos.com\/values\" rel=\"noopener noreferrer\">https:\/\/www.platformos.com\/values<\/a>  <\/p>\n\n<p>I hope you will remember the <code>map<\/code> tag and the fact that SVG has <code>a<\/code> links when such a once in a decade opportunity presents itself to use any of them. :)<\/p>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/using-webp-in-your-existing-webpage-809\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Using WebP in Your Existing Webpage<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 17 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#design<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/optimizing-images-for-the-web-18gc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Optimizing Images For The Web<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Apr 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n        <span class=\"ltag__link__tag\">#node<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n","category":["html","css"]},{"title":"Our documentation site is using Webpack 5 already","pubDate":"Thu, 15 Oct 2020 12:27:05 +0000","link":"https:\/\/dev.to\/platformos\/our-documentation-site-is-using-webpack-5-already-41eh","guid":"https:\/\/dev.to\/platformos\/our-documentation-site-is-using-webpack-5-already-41eh","description":"<p>A couple of days ago, Webpack 5 was released. Webpack is such a great tool that we decided to upgrade it on <a href=\"https:\/\/documentation.platformos.com\" rel=\"noopener noreferrer\">our documentation site<\/a> with the hope of finding and fixing some bugs to give back to its incredible community. <br>\nWe did not find any bugs, but upgrading Webpack is (and always was) a great pleasure. Excellent documentation, migration guide, changelog, support on GitHub. <\/p>\n<h3>\n  \n  \n  Issues\n<\/h3>\n\n<p>We encountered only two issues during the migration:<\/p>\n\n<ol>\n<li>Production build time is slower. About two times slower. That is not a big deal, as it will probably improve with time, and incremental builds are much faster because of the persistent cache \u2014 no big deal.<\/li>\n<li>Docsearch (search script provided by Algolia) was not working because of the missing <code>process<\/code> object. <a href=\"https:\/\/webpack.js.org\/migrate\/5\/#run-a-single-build-and-follow-advises\" rel=\"noopener noreferrer\">The is described<\/a> in Webpack 5 migration  (but you need to read it to know that), so a couple of lines of polyfill fixed that.\n<\/li>\n<\/ol>\n<div class=\"highlight js-code-highlight\">\n<pre class=\"highlight javascript\"><code>  <span class=\"k\">new<\/span> <span class=\"nx\">webpack<\/span><span class=\"p\">.<\/span><span class=\"nc\">DefinePlugin<\/span><span class=\"p\">({<\/span>\n    <span class=\"dl\">'<\/span><span class=\"s1\">process.env<\/span><span class=\"dl\">'<\/span><span class=\"p\">:<\/span> <span class=\"p\">{<\/span>\n      <span class=\"na\">NODE_ENV<\/span><span class=\"p\">:<\/span> <span class=\"nx\">JSON<\/span><span class=\"p\">.<\/span><span class=\"nf\">stringify<\/span><span class=\"p\">(<\/span><span class=\"nx\">process<\/span><span class=\"p\">.<\/span><span class=\"nx\">env<\/span><span class=\"p\">.<\/span><span class=\"nx\">NODE_ENV<\/span><span class=\"p\">),<\/span>\n    <span class=\"p\">},<\/span>\n  <span class=\"p\">})<\/span>\n<\/code><\/pre>\n\n<\/div>\n\n\n<p>Credits: <a href=\"https:\/\/github.com\/algolia\/docsearch\/issues\/980#issuecomment-708388893\" rel=\"noopener noreferrer\">https:\/\/github.com\/algolia\/docsearch\/issues\/980#issuecomment-708388893<\/a><\/p>\n<h3>\n  \n  \n  Deprecations, warnings\n<\/h3>\n\n<p>Webpack warnings and errors communicated everything else, so all the config updates were pretty easy and pointed us in the right direction when it came to cleaning it up a bit. We found two loaders and one configuration option that were not used.<\/p>\n<h3>\n  \n  \n  Build summary\n<\/h3>\n\n<p>I hope that the build summary will visually improve because it was much easier to scan it in version 4.<\/p>\n\n<p>Before:<br>\n<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%2Fi%2Fexlkv4ps5qrn8p3igw5n.jpg\" class=\"article-body-image-wrapper\"><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%2Fi%2Fexlkv4ps5qrn8p3igw5n.jpg\" alt=\"Before\"><\/a><\/p>\n\n<p>After:<br>\n<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%2Fi%2F1x7gylb9ofuq8ebj9hi0.jpg\" class=\"article-body-image-wrapper\"><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%2Fi%2F1x7gylb9ofuq8ebj9hi0.jpg\" alt=\"After\"><\/a><\/p>\n\n<p><strong>Update<\/strong>: Colors are back as of 5.1.3 :)<\/p>\n\n<p><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%2Fi%2Fxkne5c3ozp8hgsoepuqf.png\" class=\"article-body-image-wrapper\"><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%2Fi%2Fxkne5c3ozp8hgsoepuqf.png\" alt=\"5.1.3\"><\/a><\/p>\n\n\n\n<p>Hopefully, your migration will go as smoothly as ours, and your builds will be smaller. :)<\/p>\n<h2>\n  \n  \n  Read more\n<\/h2>\n\n<p>If you are interested in more performance oriented content, follow me and I promise to deliver original, or at least effective methods of improving your website. <\/p>\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/using-webp-in-your-existing-webpage-809\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Using WebP in Your Existing Webpage<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Nov 17 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#design<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/optimizing-images-for-the-web-18gc\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>Optimizing Images For The Web<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Apr 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#beginners<\/span>\n        <span class=\"ltag__link__tag\">#node<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n\n<div class=\"ltag__link\">\n  <a href=\"\/platformos\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__org__pic\">\n      <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%2Forganization%2Fprofile_image%2F2088%2F97099ebb-ac3a-457d-ac1a-d2b6aafef79e.png\" alt=\"platformOS\">\n      <div class=\"ltag__link__user__pic\">\n        <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%2Fuser%2Fprofile_image%2F51061%2Fc37084eb-e6b7-479b-8779-10e8af07b8cc.jpg\" alt=\"\">\n      <\/div>\n    <\/div>\n  <\/a>\n  <a href=\"\/platformos\/3-tips-on-preserving-website-speed-5g0c\" class=\"ltag__link__link\">\n    <div class=\"ltag__link__content\">\n      <h2>3 Tips on Preserving Website Speed<\/h2>\n      <h3>Pawe\u0142 Kowalski for platformOS \u30fb Jun 24 '20<\/h3>\n      <div class=\"ltag__link__taglist\">\n        <span class=\"ltag__link__tag\">#webperf<\/span>\n        <span class=\"ltag__link__tag\">#webpack<\/span>\n        <span class=\"ltag__link__tag\">#javascript<\/span>\n        <span class=\"ltag__link__tag\">#css<\/span>\n      <\/div>\n    <\/div>\n  <\/a>\n<\/div>\n\n\n","category":["webpack","javascript"]}]}}