{"id":296,"date":"2023-10-02T18:00:00","date_gmt":"2023-10-02T18:00:00","guid":{"rendered":"http:\/\/suimy.me\/?p=296"},"modified":"2024-05-01T17:01:44","modified_gmt":"2024-05-01T17:01:44","slug":"waterbear-building-a-free-platform-for-impactful-documentaries-part-2","status":"publish","type":"post","link":"http:\/\/suimy.me\/index.php\/2023\/10\/02\/waterbear-building-a-free-platform-for-impactful-documentaries-part-2\/","title":{"rendered":"WaterBear: Building A Free Platform For Impactful Documentaries (Part 2)"},"content":{"rendered":"

WaterBear: Building A Free Platform For Impactful Documentaries (Part 2)<\/title><\/p>\n<article>\n<header>\n<h1>WaterBear: Building A Free Platform For Impactful Documentaries (Part 2)<\/h1>\n<address>Adrian Bece<\/address>\n<p> 2023-10-02T18:00:00+00:00<br \/>\n 2024-05-01T16:05:07+00:00<br \/>\n <\/header>\n<p>In my <a href=\"https:\/\/www.smashingmagazine.com\/2023\/09\/waterbear-building-free-platform-impactful-documentaries-part1\/\">previous article<\/a>, I talked about Waterbear, a significant project I worked on as a newly-appointed lead developer, and the lessons I learned leading a team for the first time. In this second article, I\u2019ll go over some key technical highlights from the project. Before we start, let\u2019s quickly remind ourselves what WaterBear is all about and what makes it so interesting.<\/p>\n<p><a href=\"https:\/\/www.waterbear.com\">WaterBear<\/a> is a free platform bringing together inspiration and action with <strong>award-winning high-production environmental documentaries<\/strong> covering various topics, from animals and climate change to people and communities. The WaterBear team produces their own original films and documentaries and hosts curated films and content from various high-profile partners, including award-winning filmmakers, large brands, and significant non-governmental organizations (NGOs), like Greenpeace, WWF, The Jane Goodall Institute, Ellen MacArthur Foundation, Nikon, and many others.<\/p>\n<figure class=\"\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/1-waterbear-homepage.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"689\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"WaterBear homepage\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/1-waterbear-homepage.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/1-waterbear-homepage.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>For context, I am currently working at a software development company called <a href=\"https:\/\/q.agency\/\">Q Agency<\/a> based in Zagreb, Croatia. We collaborated with WaterBear and its partner companies to build a <strong>revamped and redesigned version of WaterBear\u2019s web and mobile app<\/strong> from the ground up using modern front-end technologies.<\/p>\n<p>In the first article, I briefly discussed the technical stack that includes a React-based front-end framework, <a href=\"https:\/\/nextjs.org\/\">Next.js<\/a> for the web app, Sanity CMS, Firebase Auth, and Firestore database. Definitely read up on the strategy and reasoning behind this stack in the first article if you missed it.<\/p>\n<p>Now, let\u2019s dive into the technical features and best practices that my team adopted in the process of building the WaterBear web app. I plan on sharing specifically <strong>what I learned from performance and accessibility practices as a first-time lead developer of a team<\/strong>, as well as what I wish I had known before we started.<\/p>\n<h2 id=\"image-optimization\">Image Optimization<\/h2>\n<p>Images are pieces of content in many contexts, and they are a very important and prominent part of the WaterBear app\u2019s experience, from video posters and category banners to partner logos and campaign image assets.<\/p>\n<figure class=\"\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/2-waterbear-image-cards-carousel-ui-elements.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"574\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Image assets, cards, and carousel UI elements featured on a promotional campaign page\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/2-waterbear-image-cards-carousel-ui-elements.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n Image assets, cards and carousel UI elements are featured prominently on these promotional campaign pages. (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/2-waterbear-image-cards-carousel-ui-elements.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>I think that if you are reading this article, you likely know the tightrope walk between striking, immersive imagery and performant user experiences we do as front-enders. Some of you may have even grimaced at the heavy use of images in that last screenshot. My team measured the impact, noting that on the first load, this video category page serves up as many as 14 images. Digging a little deeper, we saw those images account for approximately 85% of the total page size.<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/3-unoptimized-full-size-image.jpg\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"393\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Unoptimized, full-size JPEG which is around 1.2 MB as being transferred\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/3-unoptimized-full-size-image.jpg\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n Unoptimized, full-size JPEG is around 1.2 MB. Imagine how poor the loading performance would be if we had to load a bunch of images like this one! (<a href=\"https:\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/3-unoptimized-full-size-image.jpg\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>That\u2019s not insignificant and demands attention. WaterBear\u2019s product is visual in nature, so it\u2019s understandable that images are going to play a large role in its web app experience. Even so, 85% of the experience feels heavy-handed.<\/p>\n<p>So, my team knew early on that we would be leveraging as many image optimization techniques as we could that would help improve how quickly the page loads. If you want to know everything there is to optimize images, I wholeheartedly recommend Addy Osami\u2019s <a href=\"https:\/\/www.smashingmagazine.com\/printed-books\/image-optimization\/\"><em>Image Optimization<\/em><\/a> for a treasure trove of insightful advice, tips, and best practices that helped us improve WaterBear\u2019s performance.<\/p>\n<p>Here is how we tackled the challenge.<\/p>\n<div data-audience=\"non-subscriber\" data-remove=\"true\" class=\"feature-panel-container\">\n<aside class=\"feature-panel\">\n<div class=\"feature-panel-left-col\">\n<div class=\"feature-panel-description\">\n<p>Meet <a data-instant href=\"\/the-smashing-newsletter\/\"><strong>Smashing Email Newsletter<\/strong><\/a> with useful tips on front-end, design & UX. Subscribe and <strong>get \u201cSmart Interface Design Checklists\u201d<\/strong> \u2014 a <strong>free PDF deck<\/strong> with 150+ questions to ask yourself when designing and building almost <em>anything<\/em>.<\/p>\n<div>\n<section class=\"nlbf\">\n<div class=\"nlbwrapper\"><label for=\"mce-EMAIL-hp\" class=\"sr-only\">Your (smashing) email<\/label><\/p>\n<div class=\"nlbgroup\">\n<\/div>\n<\/div>\n<p>.c-garfield-the-cat .nlbwrapper{margin-bottom: 0;}.nlbf{display:flex;padding-bottom:.25em;padding-top:.5em;text-align:center;letter-spacing:-.5px;color:#fff;font-size:1.15em}.nlbgroup:hover{box-shadow:0 1px 7px -5px rgba(50,50,93,.25),0 3px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025)}.nlbf .nlbf-button,.nlbf .nlbf-email{flex-grow:1;flex-shrink:0;width:auto;margin:0;padding:.75em 1em;border:0;border-radius:11px;background:#fff;font-size:1em;box-shadow:none}.promo-box .nlbf-button:focus,.promo-box input.nlbf-email:active,.promo-box input.nlbf-email:focus{box-shadow:none}.nlbf-button:-ms-input-placeholder,.nlbf-email:-ms-input-placeholder{color:#777;font-style:italic}.nlbf-button::-webkit-input-placeholder,.nlbf-email::-webkit-input-placeholder{color:#777;font-style:italic}.nlbf-button:-ms-input-placeholder,.nlbf-button::-moz-placeholder,.nlbf-button::placeholder,.nlbf-email:-ms-input-placeholder,.nlbf-email::-moz-placeholder,.nlbf-email::placeholder{color:#777;font-style:italic}.nlbf .nlbf-button{transition:all .2s ease-in-out;color:#fff;background-color:#0168b8;font-weight:700;box-shadow:0 1px 1px rgba(0,0,0,.3);width:100%;border:0;border-left:1px solid #ddd;flex:2;border-top-left-radius:0;border-bottom-left-radius:0}.nlbf .nlbf-email{border-top-right-radius:0;border-bottom-right-radius:0;width:100%;flex:4;min-width:150px}@media all and (max-width:650px){.nlbf .nlbgroup{flex-wrap:wrap;box-shadow:none}.nlbf .nlbf-button,.nlbf .nlbf-email{border-radius:11px;border-left:none}.nlbf .nlbf-email{box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);min-width:100%}.nlbf .nlbf-button{margin-top:1em;box-shadow:0 1px 1px rgba(0,0,0,.5)}}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus,.nlbf .nlbf-button:hover{cursor:pointer;color:#fff;background-color:#0168b8;border-color:#dadada;box-shadow:0 1px 1px rgba(0,0,0,.3)}.nlbf .nlbf-button:active,.nlbf .nlbf-button:focus{outline:0!important;text-shadow:1px 1px 1px rgba(0,0,0,.3);box-shadow:inset 0 3px 3px rgba(0,0,0,.3)}.nlbgroup{display:flex;box-shadow:0 13px 27px -5px rgba(50,50,93,.25),0 8px 16px -8px rgba(0,0,0,.3),0 -6px 16px -6px rgba(0,0,0,.025);border-radius:11px;transition:box-shadow .2s ease-in-out}.nlbwrapper{display:flex;flex-direction:column;justify-content:center}.nlbf form{width:100%}.nlbf .nlbgroup{margin:0}.nlbcaption{font-size:.9em;line-height:1.5em;color:#fff;border-radius:11px;padding:.5em 1em;display:inline-block;background-color:#0067b859;text-shadow:1px 1px 1px rgba(0,0,0,.3)}.wf-loaded-stage2 .nlbf .nlbf-button{font-family:Mija}.mts{margin-top: 5px !important;}.mbn{margin-bottom: 0 !important;}<\/section>\n<p class=\"mts mbn\"><small class=\"promo-box__footer mtm block grey\"><em>Once a week. Useful tips on <a href=\"https:\/\/www.smashingmagazine.com\/the-smashing-newsletter\/\">front-end & UX<\/a>. Trusted by 207.000 friendly folks.<\/em><\/small><\/p>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"feature-panel-right-col\">\n<div class=\"feature-panel-image\">\n<img loading=\"lazy\" class=\"feature-panel-image-img lazyload\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Feature Panel\" width=\"310\" height=\"400\" data-src=\"\/images\/smashing-cat\/cat-firechat.svg\"><\/p>\n<\/div>\n<\/div>\n<\/aside>\n<\/div>\n<h3 id=\"using-cdn-for-caching-and-webp-for-lighter-file-sizes\">Using CDN For Caching And WebP For Lighter File Sizes<\/h3>\n<p>As I mentioned a little earlier, our stack includes Sanity\u2019s CMS. It offers a robust content delivery network (CDN) <a href=\"https:\/\/www.sanity.io\/docs\/asset-cdn\">out of the box<\/a>, which serves two purposes: (1) optimizing image assets and (2) caching them. Members of the WaterBear team are able to upload unoptimized high-quality image assets to Sanity, which ports them to the CDN, and from there, we instruct the CDN to run appropriate optimizations on those images \u2014 things like compressing the files to their smallest size without impacting the visual experience, then caching them so that a user doesn\u2019t have to download the image all over again on subsequent views.<\/p>\n<p>Requesting the optimized version of the images in Sanity boils down to adding query variables to image links like this:<\/p>\n<p><code>https:\/\/cdn.sanity.io\/...\/image.jpg?w=1280&q=70&auto=format<\/code><\/p>\n<p>Let\u2019s break down the query variables:<\/p>\n<ul>\n<li><code>w<\/code> sets the width of the image. In the example above, we have set the width to <code>1280px<\/code> in the query.<\/li>\n<li><code>q<\/code> sets the compression quality of the image. We landed on 70% to balance the need for visual quality with the need for optimized file sizes.<\/li>\n<li><code>format<\/code> sets the image format, which is set to <code>auto<\/code>, allowing Sanity to determine the best type of image format to use based on the user\u2019s browser capabilities.<\/li>\n<\/ul>\n<p>Notice how all of that comes from a URL that is mapped to the CDN to fetch a JPG file. It\u2019s pretty magical how a completely unoptimized image file can be transformed into a fully optimized version that serves as a completely different file with the use of a few parameters.<\/p>\n<p>In many cases, the <code>format<\/code> will be returned as a WebP file. We made sure to use WebP because it yields <a href=\"https:\/\/web.dev\/serve-images-webp\/\">significant savings<\/a> in terms of file size. Remember that unoptimized 1.2 MB image from earlier? It\u2019s a mere 146 KB after the optimizations.<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/4-optimized-image.jpg\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"393\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Optimized image which is around 146 kB as being transferred\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/4-optimized-image.jpg\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n With the aforementioned optimizations, we managed to reduce that unoptimized image size from 1.2 MB to 146 kB. That\u2019s over 80% reduction! (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/4-optimized-image.jpg\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>And all 14 image requests are smaller than that one unoptimized image!<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/5-table-image-requests-otimized.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"256\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"A table of 14 optimized image requests with their sizes\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/5-table-image-requests-otimized.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/5-table-image-requests-otimized.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>The fact that images still account for 85% of the page weight is a testament to just how heavy of a page we are talking about.<\/p>\n<p>Another thing we have to consider when talking about modern image formats is browser support. Although <a href=\"https:\/\/caniuse.com\/webp\">WebP is widely supported<\/a> and has been a staple for some time now, my team decided to provide an optimized fallback JPG just in case. And again, <strong>Sanity automatically detects the user\u2019s browser capabilities<\/strong>. This way, we serve the WebP version only if Sanity knows the browser supports it and only provide the optimized fallback file if WebP support isn\u2019t there. It\u2019s great that we don\u2019t have to make that decision ourselves!<\/p>\n<p>Have you heard of AVIF? It\u2019s another modern image format that promises potential savings even greater than WebP. If I\u2019m being honest, I would have preferred to use it in this project, but Sanity unfortunately does not support it, at least at the time of this article. There\u2019s a <a href=\"https:\/\/github.com\/sanity-io\/image-url\/issues\/26\">long-running ticket to add support<\/a>, and I\u2019m holding hope we get it.<\/p>\n<p>Would we have gone a different route had we known about the lack of AVIF support earlier? <a href=\"https:\/\/github.com\/sanity-io\/image-url\/issues\/26\">Cloudinary supports it<\/a>, for example. I don\u2019t think so. Sanity\u2019s tightly coupled CDN integration is too great of a developer benefit, and as I said, I\u2019m hopeful Sanity will give us that support in the future. But that is certainly the sort of consideration I wish I would have had early on, and now I have that in my back pocket for future projects.<\/p>\n<h3 id=\"tackling-the-largest-contentful-paint-lcp\">Tackling The Largest Contentful Paint (LCP)<\/h3>\n<p>LCP is the biggest element on the page that a user sees on the initial load. You want to optimize it because it\u2019s the first impression a user has with the page. It ought to load as soon as possible while everything under it can wait a moment.<\/p>\n<p>For us, images are most definitely part of the LCP. By giving more consideration to the banner images we load at the top of the page, we can serve that component a little faster for a better experience. There are a couple of modern image attributes that can help here: <code>loading<\/code> and <a href=\"https:\/\/web.dev\/fetch-priority\/#summary\"><code>fetchpriority<\/code><\/a>.<\/p>\n<p>We used <strong>an <code>eager<\/code> loading strategy<\/strong> paired with a high <code>fetchpriority<\/code> on the images. This provides the browser with a couple of hints that this image is super important and that we want it early in the loading process.<\/p>\n<pre><code class=\"language-html\"><!-- Above-the-fold Large Contentful Paint image --&gt\n<img loading=\"eager\" alt=\"...\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" width=\"1280\" height=\"720\" class=\"... lazyload\" data-src=\"...\">\n<\/code><\/pre>\n<p>We also made use of preloading in the document <code><\/code>, indicating to the browser that we want to <code>preload<\/code> images during page load, again, with <code>high<\/code> priority, using <a href=\"https:\/\/nextjs.org\/docs\/pages\/api-reference\/components\/image#priority\">Next.js image preload options<\/a>.<\/p>\n<pre><code class=\"language-html\">\n \n\n<\/code><\/pre>\n<p>Images that are \u201cbelow the fold\u201d can be de-prioritized and downloaded only when the user actually needs it. <a href=\"https:\/\/www.smashingmagazine.com\/search\/?q=Lazy%20load\">Lazy loading<\/a> is a common technique that instructs the browser to load particular images once they enter the viewport. It\u2019s only <a href=\"https:\/\/css-tricks.com\/native-lazy-loading\/\">fairly recently<\/a> that it\u2019s become a feature baked directly into HTML with the <code>loading<\/code> attribute:<\/p>\n<pre><code class=\"language-html\"><!-- Below-the-fold, low-priority image -->\n<img loading=\"lazy\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"...\" width=\"250\" height=\"350\" class=\"lazyload\" data-src=\"...\">\n<\/code><\/pre>\n<p>This cocktail of strategies made a noticeable difference in how quickly the page loads. On those image-heavy <a href=\"https:\/\/www.waterbear.com\/watch\/category\/animals\">video category pages<\/a> alone, it helped us reduce the image download size and number of image requests by <strong>almost 80% on the first load!<\/strong> Even though the page will grow in size as the user scrolls, that weight is only added if it passes through the browser viewport.<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/6-lazy-loading-strategy.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"253\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"A screenshot with image requests and their reduced download size with a lazy loading strategy\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/6-lazy-loading-strategy.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n After scrolling the video category page, a bunch more images are loaded. We reduced image download size and number of requests by almost 80% with a lazy loading strategy. (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/6-lazy-loading-strategy.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<h3 id=\"in-progress-implementing-srcset\">In Progress: Implementing <code>srcset<\/code><\/h3>\n<p>My team is incredibly happy with how much performance savings we\u2019ve made so far. But there\u2019s no need to stop there! Every millisecond counts when it comes to page load, and we are still planning additional work to optimize images even further.<\/p>\n<p>The task we\u2019re currently planning will implement the <code>srcset<\/code> attribute on images. This is not a \u201cnew\u201d technique by any means, but it is certainly a component of modern performance practices. It\u2019s also a <a href=\"https:\/\/web.dev\/learn\/design\/responsive-images\/#responsive-images-with-srcset\">key component in responsive design<\/a>, as it instructs browsers to use certain versions of an image at different viewport widths.<\/p>\n<p>We\u2019ve held off on this work only because, for us, the other strategies represented the lowest-hanging fruit with the most impact. Looking at an image element that uses <code>srcset<\/code> in the HTML shows it\u2019s not the easiest thing to read. Using it requires a certain level of art direction because the dimensions of an image at one screen size may be completely different than those at another screen size. In other words, there are additional considerations that come with this strategy.<\/p>\n<p>Here\u2019s how we\u2019re planning to approach it. We want to avoid loading high-resolution images on small screens like phones and tablets. With the <code>srcset<\/code> attribute, we can specify separate image sources depending on the device\u2019s screen width. With the <code>sizes<\/code> attribute, we can instruct the browser which image to load depending on the media query.<\/p>\n<p>In the end, our image markup should look something like this:<\/p>\n<pre><code class=\"language-html\"><img loading=\"lazy\" width=\"1280\" height=\"720\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" class=\"lazyload\" data-src=\"https:\/\/cdn.sanity.io\/...\/image.jpg?w=1280&...\">\n<\/code><\/pre>\n<p>In this example, we specify a set of three images:<\/p>\n<ol>\n<li>Small: <code>568px<\/code>,<\/li>\n<li>Medium: <code>768px<\/code>,<\/li>\n<li>Large: <code>1280px<\/code>.<\/li>\n<\/ol>\n<p>Inside the <code>sizes<\/code> attribute, we\u2019re telling the browser to use the largest version of the image if the screen width is above <code>1024px<\/code> wide. Otherwise, it should default to selecting an appropriate image out of the three available versions based on the full device viewport width (<code>100vw<\/code>) \u2014 and will do so <strong>without downloading the other versions<\/strong>. Providing different image files to the right devices ought to help enhance our performance a bit more than it already is.<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"improving-cms-performance-with-tanstack-query\">Improving CMS Performance With TanStack Query<\/h2>\n<p>The majority of content on WaterBear comes from Sanity, the CMS behind the web app. This includes video categories, video archives, video pages, the partners\u2019 page, and campaign landing pages, among others. Users will constantly navigate between these pages, frequently returning to the same category or landing page.<\/p>\n<p>This provided my team with an opportunity to introduce <strong>query caching<\/strong> and avoid repeating the same request to the CMS and, as a result, optimize our page performance even more. We used <a href=\"https:\/\/tanstack.com\/query\/v4\/docs\/react\/overview\">TanStack Query<\/a> (formerly known as <code>react-query<\/code>) for both fetching data and query caching.<\/p>\n<pre><code class=\"language-javascript\">const { isLoading, error, data } = useQuery( \/* Options *\/ )\n<\/code><\/pre>\n<p>TanStack Query caches each request according to the <a href=\"https:\/\/tanstack.com\/query\/v4\/docs\/react\/guides\/query-keys\">query key<\/a> we assign to it. The <strong>query key in TanStack Query is an array,<\/strong> where the first element is a query name and the second element is an object containing all values the query depends on, e.g., pagination, filters, query variables, and so on.<\/p>\n<p>Let\u2019s say we are fetching a list of videos depending on the video category page URL slug. We can filter those results by video duration. The query key might look something like this basic example:<\/p>\n<pre><code class=\"language-javascript\">const { isLoading, error, data } = useQuery(\n {\n queryKey: [\n 'video-category-list',\n { slug: categorySlug, filterBy: activeFilter }\n ],\n queryFn: () => \/* ... *\/\n }\n)\n<\/code><\/pre>\n<p>These query keys might look confusing at first, but they\u2019re similar to the dependency arrays for React\u2019s <a href=\"https:\/\/css-tricks.com\/intro-to-react-hooks\/#aa-create-side-effects-with-useeffect\"><code>useEffect<\/code> hook<\/a>. Instead of running a function when something in the dependency array changes, it runs a query with new parameters and returns a new state. TanStack Query comes with its dedicated <a href=\"https:\/\/tanstack.com\/query\/v4\/docs\/react\/devtools\">DevTools<\/a> package. It displays all sorts of useful information about the query that helps debug and optimize them without hassle.<\/p>\n<p>Let\u2019s see the query caching in action. In the following video, notice how data loads instantly on repeated page views and repeated filter changes. Compare that to the first load, where there is a slight delay and a loading state before data is shown.<\/p>\n<figure class=\"video-embed-container break-out\">\n<div class=\"video-embed-container--wrapper\"><\/div>\n<\/figure>\n<p>Now, we\u2019ve optimized performance on the fetching side of things, the markup side of things, and the query side of things. That\u2019s a nice spread of coverage the team can be proud of.<\/p>\n<h2 id=\"accessibility-efforts\">Accessibility Efforts<\/h2>\n<p>Accessibility is hard! That\u2019s not to knock it, but to recognize the importance of all the work that it entails. The realm of accessibility is much bigger than making sure <code>alt<\/code> text is used on images and screen readers announcing things in order. It\u2019s a practice unto itself and is directly influenced by the front-end work we do. Even unintentionally, our code can, at best, adversely impact a user\u2019s ability to access certain content and, at worst, make it completely impossible to navigate a site with assistive technology.<\/p>\n<p>This being my first significant project as a lead developer, I wanted the quality of the code my team writes (based on The Four Pillars outlined in the first article of this series) to consider accessibility with special attention to not ruining something that already worked.<\/p>\n<p>That extends to the decisions we make when implementing new features, particularly those that leverage third-party resources.<\/p>\n<blockquote class=\"pull-quote\">\n<p>\n <a class=\"pull-quote__link\" aria-label=\"Share on Twitter\" href=\"https:\/\/twitter.com\/share?text=%0aWhether%20a%20resource%20provides%20solid%20a11y%20features%20right%20out%20of%20the%20box%20was%20a%20key%20determining%20factor%20in%20what%20solutions%20we%20chose%20for%20the%20WaterBear%20app.%0a&url=https:\/\/smashingmagazine.com%2f2023%2f10%2fwaterbear-building-free-platform-documentaries-part2%2f\"><\/p>\n<p>Whether a resource provides solid a11y features right out of the box was a key determining factor in what solutions we chose for the WaterBear app.<\/p>\n<p> <\/a>\n <\/p>\n<div class=\"pull-quote__quotation\">\n<div class=\"pull-quote__bg\">\n <span class=\"pull-quote__symbol\">\u201c<\/span><\/div>\n<\/p><\/div>\n<\/blockquote>\n<p>For example, it\u2019s probably obvious that an app for documentaries is going to be designed around video. We made sure to choose a video player with a decent level of a11y support that allows users full keyboard control, uses clear focus states, adds basic <code>aria-*<\/code> attributes <a href=\"https:\/\/www.a11y-collective.com\/the-first-rule-for-using-aria\/\">where necessary<\/a>, includes <a href=\"https:\/\/hidde.blog\/using-javascript-to-trap-focus-in-an-element\/\">focus-trapping<\/a> within modal components, writes semantically-correct HTML\u2026 you get the idea. There\u2019s a lot to consider.<\/p>\n<figure class=\"\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/7-focus-state-green-outline.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"689\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"A screenshot with a number of cards with one of them being focused with a green outline\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/7-focus-state-green-outline.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n Notice the prominent green outline on the focused card. Developers tend to hide the focus state, which is a bad practice. It\u2019s more beneficial to customize it to fit the design. (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/7-focus-state-green-outline.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>We\u2019re probably not even covering all of our bases! It\u2019s so tough to tell without ample user testing. It\u2019s a conflicting situation where you want to do everything you can while realistically completing the project with the resources you have and proceed with intention.<\/p>\n<p>We made sure to include a label on interactive elements like buttons, especially ones where the icon is the only content. For that case, we added <a href=\"https:\/\/www.a11yproject.com\/posts\/how-to-hide-content\/\">visually hidden<\/a> text while allowing it to be read by assistive devices. We also made sure to <a href=\"https:\/\/www.smashingmagazine.com\/2021\/05\/accessible-svg-patterns-comparison\/\">hide the SVG<\/a> icon from the assistive devices as SVG doesn\u2019t add any additional context for assistive devices.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-html\"><!-- Icon button markup with descriptive text for assistive devices -->\n<button type=\"button\" class=\"...\">\n ...<span class=\"visually-hidden\">Open filters<\/span>\n<\/button>\n<\/code><\/pre>\n<\/div>\n<pre><code class=\"language-css\">.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n white-space: nowrap;\n clip: rect(0 0 0 0);\n -webkit-clip-path: inset(50%);\n clip-path: inset(50%);\n}\n<\/code><\/pre>\n<p>Supporting keyboard navigation was one of our accessibility priorities, and we had no trouble with it. We made sure to use proper HTML markup and avoid potential pitfalls like adding a click event to meaningless <code>div<\/code> elements, which is unfortunately so easy to do in React.<\/p>\n<p>We did, however, hit an obstacle with modals as users were able to move focus outside the modal component and continue interacting with the main page while the modal was in its open state, which isn\u2019t possible with the default pointer and touch interaction. For that, we implemented focus traps using the <a href=\"https:\/\/www.npmjs.com\/package\/focus-trap-react\">focus-trap-react library<\/a> to keep the focus on modals while they\u2019re opened, then restore focus back to an active element once the modal is closed.<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/8-focus-trapping.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"465\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"A screenshot with a modal, where you can choose a story length: any, short, medium or long\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/8-focus-trapping.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n With focus trapping, we ensure that the browser focus stays inside the modal while it\u2019s opened and is restored back to the previous element once the moda is closed. (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/8-focus-trapping.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"dynamic-sitemaps\">Dynamic Sitemaps<\/h2>\n<p><strong>Sitemaps tell search engines<\/strong> <a href=\"https:\/\/developers.google.com\/search\/docs\/crawling-indexing\/sitemaps\/build-sitemap\"><strong>which pages to crawl<\/strong><\/a>. This is faster than just letting the crawler discover internal links on its own while crawling the pages.<\/p>\n<p>The importance of sitemaps in the case of WaterBear is that the team regularly publishes new content \u2014 content we want to be indexed for crawlers as soon as possible by adding those new links to the top of the sitemap. We don\u2019t want to rebuild and redeploy the project every time new content has been added to Sanity, so <strong>dynamic server-side sitemaps were our logical choice<\/strong>.<\/p>\n<p>We used the <a href=\"https:\/\/www.npmjs.com\/package\/next-sitemap\">next-sitemap<\/a> plugin for Next.js, which has allowed us to easily configure the sitemap generation process for both static and dynamic pages. We used the plugin alongside custom Sanity queries that fetch the latest content from the CMS and quickly generate a fresh sitemap for each request. That way, we made sure that the latest videos get indexed as soon as possible.<\/p>\n<p>Let\u2019s say the WaterBear team publishes a page for a video named <em>My Name is Salt<\/em>. That gets added to a freshly generated XML sitemap:<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/9-example-dynamic-sitemap-latest-video-content.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"504\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"An example of a dynamic sitemap for the latest video content\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/9-example-dynamic-sitemap-latest-video-content.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n This is an example of a dynamic sitemap for the latest video content. (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/9-example-dynamic-sitemap-latest-video-content.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<p>Now, it\u2019s indexed for search engines to scoop up and use in search results:<\/p>\n<figure class=\"\n \n break-out article__image\n \n \n \"><\/p>\n<p> <a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/10-indexed-video-sitemap.png\"><\/p>\n<p> <img loading=\"lazy\" width=\"800\" height=\"212\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"An example of the recently indexed video from the sitemap with the \u201c6 days ago\u201d info\" class=\"lazyload\" data-src=\"https:\/\/res.cloudinary.com\/indysigner\/image\/fetch\/f_auto,q_80\/w_400\/https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/10-indexed-video-sitemap.png\"><\/p>\n<p> <\/a><figcaption class=\"op-vertical-bottom\">\n This is an example of the recently indexed video from the sitemap. Notice the \u201c6 days ago\u201d info, which corresponds to the <code>lastmod<\/code> value in the sitemap. (<a href=\"https:\/\/files.smashing.media\/articles\/waterbear-building-free-platform-documentaries-part2\/10-indexed-video-sitemap.png\">Large preview<\/a>)<br \/>\n <\/figcaption><\/figure>\n<h2 id=\"until-next-time\">Until Next Time\u2026<\/h2>\n<p>In this article, I shared some insights about <a href=\"https:\/\/www.waterbear.com\/\">WaterBear<\/a>\u2019s tech stack and some performance optimization techniques we applied while building it.<\/p>\n<p>Images are used very prominently on many page types on WaterBear, so we used CDN with caching, loading strategies, preloading, and the WebP format to optimize image loading performance. We relied on Sanity for the majority of content management, and we expected repeating page views and queries on a single session, prompting us to implement query caching with TanStack Query.<\/p>\n<p>We made sure to improve basic accessibility on the fly by styling focus states, enabling full keyboard navigation, assigning labels to icon buttons, providing alt text for images, and using focus traps on modal elements.<\/p>\n<p>Finally, we covered how my team handled dynamic server-side rendered sitemaps using the next-sitemap plugin for Next.js.<\/p>\n<p>Again, this was my first big project as lead developer of a team. There\u2019s so much that comes with the territory. Not only are there internal processes and communication hurdles to establish a collaborative team environment, but there\u2019s the technical side of things, too, that requires balancing priorities and making tough decisions. I hope my learning journey gives you something valuable to consider in your own work. I know that my team isn\u2019t the only one with these sorts of challenges, and sharing the lessons I learned from this particular experience probably resonates with some of you reading this.<\/p>\n<p>Please be sure to check out the full work we did on WaterBear. It\u2019s available on the <a href=\"https:\/\/www.waterbear.com\/\">web<\/a>, <a href=\"https:\/\/play.google.com\/store\/apps\/details?id=com.waterbearnetwork.waterbear&hl=en_US&pli=1\">Android,<\/a> and <a href=\"https:\/\/apps.apple.com\/us\/app\/waterbear\/id1461149595\">iOS<\/a>. And, if you end up watching a documentary while you\u2019re at it, let me know if it inspired you to take action on a cause!<\/p>\n<h3 id=\"references\">References<\/h3>\n<ul>\n<li><a href=\"https:\/\/web.dev\/fast\/#optimize-your-images\">Fast load times<\/a> (web.dev)<\/li>\n<li>\u201c<a href=\"https:\/\/www.a11yproject.com\/posts\/how-to-hide-content\/\">Hide Content<\/a>,\u201d Dave Rupert (The A11Y Project)<\/li>\n<li>\u201c<a href=\"https:\/\/web.dev\/serve-images-webp\/\">Use WebP images<\/a>,\u201d Katie Hempenius (web.dev)<\/li>\n<li>\u201c<a href=\"https:\/\/www.smashingmagazine.com\/2021\/05\/accessible-svg-patterns-comparison\/\">Accessible SVGs: Perfect Patterns For Screen Reader Users<\/a>,\u201d Carie Fisher<\/li>\n<li><a href=\"https:\/\/tanstack.com\/query\/v4\/docs\/react\/overview\">TanStack Query Documentation<\/a><\/li>\n<\/ul>\n<p><em>Many thanks to WaterBear and Q Agency for helping out with this two-part article series and making it possible. I really would not have done this without their support. I would also like to commend everyone who worked on the project for their outstanding work! You have taught me so much so far, and I am grateful for it.<\/em><\/p>\n<div class=\"signature\">\n <img src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Smashing Editorial\" width=\"35\" height=\"46\" loading=\"lazy\" class=\"lazyload\" data-src=\"https:\/\/www.smashingmagazine.com\/images\/logo\/logo--red.png\"><br \/>\n <span>(gg, yk, il)<\/span>\n<\/div>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>WaterBear: Building A Free Platform For Impactful Documentaries (Part 2) WaterBear: Building A Free Platform For Impactful Documentaries (Part 2) […]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[12],"tags":[],"_links":{"self":[{"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/posts\/296"}],"collection":[{"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/comments?post=296"}],"version-history":[{"count":1,"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/posts\/296\/revisions"}],"predecessor-version":[{"id":297,"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/posts\/296\/revisions\/297"}],"wp:attachment":[{"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/media?parent=296"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/categories?post=296"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/suimy.me\/index.php\/wp-json\/wp\/v2\/tags?post=296"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}