Shuhei Kagawa

Responsive images with a static site generator

Jan 26, 2020 - Blog, JavaScript

Responsive images

An img without width/height attributes causes a page jump when it's loaded. It happens because the browser doesn't know the dimensions of the image until its data is loaded—only the first part that contains dimensions is enough though. It's common to specify width and height of img tag to avoid the jump.

<img src="/images/foo.jpg" alt="Foo" width="800" height="600" />

But img tag with width and height doesn't always work well with Responsive Design because the dimensions are fixed. I wanted images to fit the screen width on mobile phones. So I left images without width/height and let them cause page jumps.

Recently, I came across a similar problem at work and learned a cool technique to create a placeholder of the image's aspect ratio with padding-top. If the image's aspect ratio is height/width = 75/100:

<div style="position: relative; padding-top: 75%;">
  <img style="position: absolute; top: 0; left: 0; max-width: 100%;" />
</div>

The div works as a placeholder with the image's aspect ratio that fits the width of its containing element. Because the img tag has position: absolute, it doesn't cause a page jump when it's loaded.

I decided to implement it on this blog. This blog is made with a custom static site generator. I'm not sure if it's useful for anyone else, but I write how I did it anyway…

Limiting overstretch

In addition to images that are wide enough to always fill the full width of the content area, I had images that are not wide enough to fill the full width of a laptop, but wide enough to fill the full width of a mobile phone. Not to stretch the image on laptops, I decided to go with another wrapper to limit the maximum width of the image. If the image's width is 500px:

<div style="max-width: 500px;">
  <div style="position: relative; padding-top: 75%;">
    <img style="position: absolute; top: 0; left: 0; max-width: 100%;" />
  </div>
</div>

Getting image dimensions

The placeholder technique requires image dimensions. I used image-size module to get image dimensions.

The following function gets dimensions of images in a directory and returns them as a Map.

const util = require("util");
const path = require("path");
const { promises: fs } = require("fs");
const sizeOf = util.promisify(require("image-size").imageSize);

async function readImageSizes(dir) {
  const files = (await fs.readdir(dir)).filter(f => !f.startsWith("."));
  const promises = files.map(async file => {
    const filePath = path.resolve(dir, file);
    const dimensions = await sizeOf(filePath);
    return [file, dimensions];
  });
  const entries = await Promise.all(promises);
  return new Map(entries);
}

Custom renderer of Marked

This blog's posts are written in Markdown, and its static site generator uses marked to convert Markdown into HTML. One of my favorite things about marked is that we can easily customize its behavior with a custom renderer.

I used span tags to wrap img tag because they are rendered in p tag, which can't contain block tags like div.

class CustomRenderer extends marked.Renderer {
  image(src, title, alt) {
    const dimensions = this.imageDimensions && this.imageDimensions.get(src);
    if (dimensions) {
      const { width, height } = dimensions;
      const aspectRatio = (height / width) * 100;
      return (
        `<span class="responsive-image-wrapper" style="max-width: ${width}px;">` +
        `<span class="responsive-image-inner" style="padding-top: ${aspectRatio}%;">` +
        `<img class="responsive-image" src="${src}" alt="${alt}">` +
        "</span>" +
        "</span>"
      );
    }
    return super.image(src, title, alt);
  }

  // To set images dimensions when images are changed
  setImageDimensions(imageDimensions) {
    this.imageDimensions = imageDimensions;
  }
}

And CSS:

.responsive-image-wrapper {
  display: block;
}
.responsive-image-inner {
  display: block;
  position: relative;
}
.responsive-image {
  position: absolute;
  top: 0;
  left: 0;
}

Result

Here are a few examples:

Yay, no more page jump! Well, web fonts still make the page slightly jump, but that's another story...

I skipped some details that are specific to my website. The full code is on GitHub.

Winter terminal (mostly Vim) cleaning

Dec 31, 2019 - Vim

In December, I spent some time cleaning up my terminal setup. Dust had piled up in a year, and my terminal was getting slower. It was time to dust off.

Here are highlights of the changes.

Faster text rendering

I noticed a non-negligible lag when I was editing JavaScript/TypeScript in Neovim. At first, I thought some Vim plugins caused it. But it was not true. Not only editing was slow, but also scrolling was slow. Text rendering itself was the problem.

I opened files of different types in Vim's vertical split and less in tmux's vertical split. And I scrolled down and (subjectively) evaluated the smoothness of scrolling.

It turned out that Vim was not the problem. With vertical splits of tmux, even less command was slow to scroll. Regardless of Vim or tmux, text rendering in vertical splits was slow on iTerm2. In retrospect, it makes sense because iTerm2 doesn't know about vertical split by Vim or tmux and can't limit rendering updates to the changed pane. iTerm2's tmux integration may have helped, but I didn't try that.

I tried Alacritty, and it was much faster! I had been using Alacritty before but switched back to iTerm2 for font ligatures. Now I didn't care much about font ligatures—ligatures look pretty, but glyphs for != and !== confused me in JavaScript. So I switched to Alacritty again.

Also, I stopped using flatlandia color scheme in Vim, and it improved the rendering speed a bit. I didn't dig into why, though.

fzf.vim

fzf.vim was a life changer. It provides a blazing fast incremental search for almost anything. I use it for file names (instead of ctrlp.vim), commit history and grep. Especially, incremental grep with a preview is amazing.

More Vim cleaning

  • Started using ale as a Language Server Protocol client. I was using ale for linting and fixing, and LanguageClient-neovim for LSP features. LanguageClient-neovim also shows a quickfix window when a file contains syntax errors and was conflicting with ale. I learned that ale supported LSP as well and made it handle LSP too.

    • Update on Jan 3, 2020: I started using coc.nvim instead of ale and deoplete.nvim for autocomplete, linting, fixing and LSP features. It makes Vim an IDE. Simply incredible.
  • Configured Vim to open :help in a vertical split. :help is a valuable resource when configuring Vim. The problem for me was that Vim opens help in a horizontal split by default. Opening help in a vertical split makes it much easier to read.

    autocmd FileType help wincmd H
    
  • Sorted out JavaScript/JSX/TypeScript syntax highlighting. Vim sets javascriptreact to .jsx and typescriptreact to .tsx by default. But those file types don't work well with the plugin ecosystem because plugins for javascript/typescript file types don't work with javascriptreact/typescriptreact and popular JSX/TSX plugins use javascript.jsx and typescript.tsx.

    autocmd BufRead,BufNewFile *.jsx set filetype=javascript.jsx
    autocmd BufRead,BufNewFile *.tsx set filetype=typescript.tsx
    
  • Stopped unnecessarily lazy-loading Vim plugins with dein.vim. I had configured file-type-specific plugins as lazy plugins of dein.vim without understanding much. The truth was that lazy plugins are meaningful only for plugins with plugin directory. Most of the file-type-specific plugins don't have plugin directory and are lazily loaded by default with ftdetect and ftplugin. :echo dein#check_lazy_plugins() shows those plugins that are ill-configured. I finally learned what those plugin directories do after using Vim for several years...

  • Reviewed key mappings and removed waiting time by avoiding mappings that prefixed other mappings. For example, I had mappings of ,g and ,gr. ,g was slow because Vim had to wait for a while to determine it was ,g or ,gr.

  • Tried Vim 8 but switched back to Neovim. Vim 8 worked well, but tiny details looked smoother in Neovim. For example, when syntax highlighting hangs up, Vim 8 hangs up while Neovim disables syntax highlighting and goes on.

  • Started documentation of my setup. I keep forgetting key mappings, useful plugins that I occasionally use, how things are set up, etc.

2019 in review

Dec 31, 2019 - Review

Aegina island in April

Travels

I visited six new countries and enjoyed each of them.

  • January: Japan
  • March: Budapest, Hungary
  • April: Athens and Aegina, Greece
  • April: Wrocław, Poland
  • June: Prague, Czech Republic
  • August: Brussels and Bruges, Belgium
  • September: Munich, Germany
  • October: Dubrovnik, Croatia
  • November: Japan

There are many more places to visit in Europe. I'll keep traveling in 2020.

Bike

I bought an entry-level road bike at Decathlon in June. I used it for commuting and made day trips around Berlin. Berlin is surrounded by amazing fields. Blankenfelde is my favorite so far. I can't wait for the next Spring.

Budgeting

I started using YNAB at the end of the last year because a few of my friends were using it. I have used it throughout the year, and it's the first budgeting system that worked well for me. It helps us traveling regularly while saving money.

Books

I bought 55 physical books and 5 ebooks. That's three times more than the last year, probably because of a dedicated budget for buying books.

I finished reading 14 books and stranded somewhere in the middle of many books. Quiet made me more introverted and now I spend more time at home. Bad Blood and Educated blew my mind.

Drink

While it's hard to name the best food of the year, the best beer of 2019 was a porter at Kontynuacja in Wrocław, Poland. The city had high-quality craft beers, and everything that I drank was amazing.

I had a chance to meet one of my most favorite brewers, Fuerst Wiacek, at a tap takeover event at Biererei Bar. I bought a T-shirt.

The trip to Brussels was epic as well because I visited a traditional lambic brewery where they still brew beer with yeasts in the air of the building! After tasting sour lambic, I bought a T-shirt there too.

In spite of the encounters with good beers, I don't drink as much as I used to anymore. When I started commuting by bike, I didn't want to get drunk and ride a bike. Even after I stopped commuting with my bike in the winter, the momentum kept going. I drank almost every day in Japan as an exception, but I almost quit drinking again after coming back to Berlin.

On the other hand, I started drinking bubble tea regularly. There is a Comebuy shop near my office. I tried several bubble tea shops in Tokyo and found that only a few were better than Comebuy—I liked Yi Fang most.

Work

I am still working on the same team in Zalando. I looked for new opportunities inside and outside the company but decided to stay a bit more.

My team and I started on-call duties. Before that, another on-call team was taking care of my team's applications, and we tried to make sure that they didn't get called. Our focus on reliability hasn't changed much, but being on-call triggered new learnings. Writing post-mortem documents is my new favorite activity.

I started interviewing regularly, mostly on coding. I'm still not used to it, and there's more to learn.

Side projects

In the first half of the year, I wrote a small metrics utility for work and a toy interpreter and compiler in Rust for fun. In the last half, I focused more on learning classic algorithms than side projects.

Conferences and meetups

I attended two conferences, JSConf EU and JSConf JP, and had a chance to speak at a meetup.

2020

Looking back, 2019 was fun. I wish 2020 would be a happy year for all of you!