Posts tagged code

Vue Inner Image Zoom v2

As promised, now that migrating is easier I’ve updated my Vue Inner Image Zoom component to support Vue 3. If you’re still on Vue 2 and want to use the component, just make sure to install it as vue-inner-image-zoom@1.1.1.

I also updated the demos site to remove the lazy loading example since vue-lazyload isn’t compatible with Vue 3 (I’m open to any suggestions for replacements) and switched from vue-slick-carousel to Swiper both for compatibility and because it’s my preferred carousel library.

If I broke anything and you run into any new bugs, please report them on the GitHub issues page.

The Mysterious Case of Emotion and “exports is not defined”

Thought I’d share a bug I ran into a while back that sent me on a Poirot style investigation full of red herrings and unexpected culprits.

This is tangentially related to my recent page speed woes at work. We’d started using Emotion for CSS-in-JS in our component library and, combined with lazy and conditional component loading, it helped with some of the “Reduce unused CSS” warnings we were seeing in Lighthouse.

So adding Emotion as a styling option in our main codebase seemed like an obvious choice. We’d already installed @emotion/core (v10) when we started importing from our component library which meant it should be a quick, two-step process: 1) running yarn add @emotion/babel-preset-css-prop and 2) adding it to our babel.config.js presets after @babel/preset-react. I followed those steps, ran Webpack, and promptly got the error “ReferenceError: exports is not defined”.

Weird.

Continue Reading

Moving a Node App from Google Compute Engine to DigitalOcean

This site can

There’s nothing like trying to deploy a minor site update and having your VM hang while SSHing 75% of the time, give up mid-build the other 25%, and then fall apart completely when you try to restart it. Since all I remember about setting up a Google Compute Engine Node instance in the first place is that it took me a while and wasn’t particularly intuitive, this seemed like a good opportunity to try a new host. I’m running a commercial Next.js site with a custom server so after browsing around DigitalOcean seemed like the best option.

And it was actually pretty painless!

Of course, it would be even quicker with a step-by-step guide so here’s a reference for my future self or anyone else who wants it.

My guide for migrating a Node app to DigitalOcean (this may vary a little depending on your site):

  1. Sign up for DigitalOcean and create a Droplet using NodeJS 1-Click App. Poking around the DigitalOcean Community questions, I ran into some talk about additional installations for production but my Droplet came with Nginx, PM2, and optional Let’s Encrypt for SSL out of the box so those discussions may be a little out of date.
  2. Add the new IPv4 address to MongoDB Atlas. DigitalOcean provides instructions for getting started with your Droplet but I wound up skipping around a bit. Since I already had a MongoDB database set up, I completely ignored their section on creating a new one. To use an existing database, just remember to log in to MongoDB Atlas and add your new IP address (using port 22) to the IP Address List under Network Access.
  3. Clone your repo. Using ssh root@0.0.0.0 (replacing the zeros with your IP address), ssh into your Droplet and clone your repo.
  4. Copy environmental variable files, assets, etc. From outside your Droplet, use scp -r /path/on/computer root@0.0.0.0:/path/on/droplet (replacing the paths and zeros) to copy any necessary files that weren’t included in the repo.
  5. Start your app. Back in the Droplet, start with a good old yarn install. Then, for a Next.js site, run yarn build followed by sudo NODE_ENV=production -u nodejs pm2 start server.js to start a custom server. This is also a good time to jump back to the DigitalOcean instructions for shutting down the starter app with sudo -u nodejs pm2 delete hello and saving your PM2 process list with sudo -u nodejs pm2 save.
  6. Create an Nginx configuration. I added “read up on everything Nginx can do” to my todo list but for now I have a pretty simple config with basic asset caching in my /etc/nginx/sites-enabled/ directory. You can view the file in this Gist but keep in mind it’s pretty tailored to my SSL setup. Whether you alter my example, the DigitalOcean default, or start fresh, make sure to 1) switch the root setting to something like /root/your-site/.next/pages; for Next.js and 2) update the proxy_pass to use your app’s port (8443 in my example). Restart with sudo systemctl restart nginx.
  7. Point your domain to the new IPv4 address. How you do this will depend on your DNS host but you’ll want to complete the migration by updating your A name to point to your new IP address.

There it is in seven steps. The first run through took a bit of trial and error but hopefully this will speed things up next time.

Preloading Images in a Responsive, WebP World

Watch out for giant images

It’s time for another Google Core Web Vitals themed TIL. This one concerns image loading. Specifically, huge, above the fold image preloading.

The site I work on uses a lot of super wide to full width hero images. We’d done some previous optimizations around serving images in WebP format (Serve images in modern formats) and using picture and srcset for responsive images (Properly size images). I won’t go into details since those topics already have a ton of tutorials that explain things better than I ever could. In fact, Smashing Magazine alone has you covered with Using WebP Image Format Today and Responsive Images Done Right: A Guide To picture And srcset.

Preload critical assets to improve loading speed.

The one downside to our earlier work? It made things a little more confusing when we started looking into preloading images.

Preloading assets that aren’t actually used isn’t particularly helpful (Chrome will even warn you if you do it!). If you’re serving a WebP image but preloading the fallback or preloading a mobile image on a desktop browser, you aren’t getting any benefits out of it.

Let’s say you have a picture block that selects an image based on browser support, screen resolution, and breakpoint:

<picture>
  <source srcset="large-image.webp, large-image-2x.webp 2x" media="(min-width: 768px)" type="image/webp" />
  <source srcset="large-image.jpg, large-image-2x.jpg 2x" media="(min-width: 768px)" />
  <source srcset="small-image.webp, small-image-2x.webp 2x" type="image/webp" />
  <img src="fallback-image.jpg" srcset="small-image.jpg, small-image-2x.jpg 2x" />
</picture>

The first question is how to handle different image formats. Looking at the Can I use entries for Webp image formats and preload, it seems pretty unlikely for a browser to support preloading but not WebP images. And according to a quick aside in this Addy Osmani article on hero images, you can use the type="image/webp" attribute with a link tag to specify that you want to preload a WebP image.

Next, we need to deal with screen sizes and resolutions. In addition to type, the link tag supports media and imagesrcset attributes which should look very similar to the media and srcset attributes on our source and img tags. The main difference is that you have to be more explicit with your breakpoints since the browser won’t be choosing the best image itself.

Putting that all together, you would add this code to your head tag to preload the images from the example above:

<link rel="preload" as="image" href="large-image.webp" media="(min-width: 768px)" imagesrcset="large-image.webp, large-image-2x.webp 2x" type="image/webp" />
<link rel="preload" as="image" href="small-image.webp" media="(max-width: 767px)" imagesrcset="small-image.webp, small-image-2x.webp 2x" type="image/webp" />

If you prefer an example with real images, check out this Pen on Codepen. Just click on Settings and HTML to view the preloading the code.

Optimizing Lodash imports with jscodeshift

Since the beginning of the year, I’ve spent a lot of time at work getting ready for Google Core Web Vitals-ageddon. Most of the techniques we’ve tried are fairly well-documented and I don’t want to retread the great advice and tutorials that are already out there (although I should put together a roundup of links). A few have required a little more investigation and experimentation, though, and those seemed worth writing up.

Remove unused JavaScript! Avoid enormous network payloads!

One easy trick for creating huge JavaScript bundles and making Google angry is importing the entire Lodash library when you only use a few methods. A lot has been written on Lodash and bundle sizes and best practices for imports (I’m partial to The Correct Way to Import Lodash Libraries - A Benchmark on BlazeMeter) but what I found lacking were tips on how to update an older, monolithic Rails app with inconsistent import patterns and the continual risk of unmanageable merge conflicts.

Enter jscodeshift.

jscodeshift is a toolkit that allows you to run codemods over JavaScript files and it was a lifesaver in this situation. According to the article above, direct imports are the way to go and jscodeshift makes it possible to instantly transform any files:

  • Importing the full library (import _ from 'lodash')
  • Importing methods from Lodash with curly brackets (import { name } from 'lodash')
  • Calling methods starting with _.

To get started with jscodeshift, run npm install -g jscodeshift to install it globally and follow along below.

Continue Reading

React Inner Image Zoom v2.0.0

Well. Apparently way back in July when I published react-inner-image-zoom v1.1.1, I announced it with a long intro about trying to get motivated during the lockdown and my hope that it would kick off a glorious new age of productivity.

That didn’t actually happen, but I’m sure it will now that we’re a year into lockdown and I actually finished v2.0.0 after putting it off for months.

So what do you need to know about the new version? It:

  • Refactors the code using React hooks so you’ll need React v16.8.0 or above to upgrade.
  • Renames the startsActive prop to zoomPreload.
  • Adds hideCloseButton and hideHint props to hide those elements. If the close button is hidden, zoom out will be triggered by tap on mobile.
  • Adds width, height, and hasSpacer props to make Lighthouse happy. The width and height will be added as attributes on the original image. If hasSpacer is true, those values will be used to get the original image’s aspect ratio and add a loading spacer to prevent cumulative layout shift.

If you find any bugs, be sure to submit an issue on Github.