Next.js: next-optimized-images を使った画像自動最適化

/

※ nextjs 画像最適化のための next/imageImage component が登場して以来、該当ライブラリは開発を停止しています。

今回は画像を webp 等に変換し、レスポンシブや Low Quality Image Placeholder に対応するといった、画像最適化について書く。

next-optimized-images

以降、next-optimized-images を”next-opti”と略す

画像の最適化方法はタスクランナーのなかで画像圧縮プラグインを利用するなど複数あるが、今回は cyrilwanner/next-optimized-images を利用する。

environment

  • “node”: “v14.5.0”
  • “react”: “16.13.1”
  • “next”: “9.3.5”,
  • “next-optimized-images”: “^2.6.2”

canary版のv3 もある。

setup

undefined
1
yarn add next-optimized-images

このパッケージに加え、自分が必要な機能にあったプラグインを入れる必要がある。取り敢えず、MozJPEG と OptiPNG に変換するプラグインを入れておく。

undefined
1
yarn add npm imagemin-mozjpeg imagemin-optipng

Config

パッケージの方に 各プラグインのデフォルト設定値 が含まれているが、next.config.js のなかで設定を変更できる。下は自分のもので書きやすくするために、next-compose-plugins を入れている。

next.config.js
1
const withPlugins = require('next-compose-plugins')
2
const optimizedImages = require('next-optimized-images')
3
4
const nextOptimizedImagesConfig = {
5
imagesFolder: 'images',
6
imagesName: '[name]-[hash].[ext]',
7
handleImages: ['jpeg', 'png', 'webp'],
8
removeOriginalExtension: true,
9
optimizeImages: process.env.MODE_ENV !== 'development',
10
optimizeImagesInDev: false,
11
mozjpeg: { quality: 85, },
12
optipng: { optimizationLevel: 3, },
13
webp: { preset: 'default', quality: 85,},
14
}
15
16
module.exports = withPlugins(
17
[
18
[ optimizedImages, nextOptimizedImagesConfig ],
19
],
20
)

usage

href (image path)

next.js.config にパスのエイリアスを設定し、 <img src={require(../../example.jpg)} /> の様に指定する。

Image from Gyazo

原因は webpack にある模様

next.config.js
1
const { resolve } = require('path')
2
3
const nextConfig = {
4
webpack: (config) => {
5
config.resolve.alias['@public/assets'] = resolve(__dirname, 'public/assets')
6
return config
7
},
8
}
9
10
// ...
11
module.exports = withPlugins([ ... ], nextConfig)

convert to webp

undefined
1
yarn add webp-loader

また、imagemin-mozjpeg や imagemin-optipng 等は href={require('../example.jpg')} の様にすればプラグインが適用化される。が、その他は query params で指定する必要がある。

undefined
1
export default () => (
2
<picture>
3
<source srcSet={require('./images/my-image.jpg?webp')} type="image/webp" />
4
<img src={require('./images/my-image.jpg')} />
5
</picture>
6
)

responsive image

undefined
1
yarn add responsive-loader sharp

resize を可能にする responsive-loarder は jimp と sharp が別に必要だが、jimp は README.md(↓)でディスられてる位なので、sharp を使う。

Requires the optional package responsive-loader (npm install responsive-loader) and either jimp (node implementation, slower) or sharp (binary, faster)

画像をリンクする際は require('./images/my-image.jpg?resize&sizes[]=300&sizes[]=600&sizes[]=1000') の様に指定できる。が、下の様に responsive:sized:[] と画像サイズ幅を global resize property として指定できる。

next.config.js
1
// ...
2
const nextOptimizedImagesConfig = {
3
// ...
4
responsive: {
5
adapter: require('responsive-loader/sharp'),
6
sizes: [640, 960, 1200, 1800],
7
disable: process.env.MODE_ENV === 'development'
8
},
9
// ...
10
}
11
12
module.exports = withPlugins(
13
[[ optimizedImages, nextOptimizedImagesConfig ],],
14
nextConfig
15
)

サンプルコード ※ require 内での sizes 指定方法は下の様に複数ある。

undefined
1
// どれでも動く
2
const multi = require(`../../public/cat1200x.jpg?resize&sizes:[640,960,1200,1800]`)
3
// const multi = require('../../public/cat1200x.jpg?resize&sizes[]=640&sizes[]=960&sizes[]=1200&sizes=[1800]')
4
// const multi = require('../../public/cat1200x.jpg?resize&sizes[]=640,sizes[]=960,sizes[]=1200,sizes[]=1900')
5
6
export default () => (
7
<img
8
srcSet={multi.srcSet}
9
src={multi.src}
10
width={multi.width}
11
height={multi.height}/>
12
);
Image from Gyazo

webp-loader と responsive-loader

現状の next-opti は webp-loader と responsive-loader を example.jpg?webp?resize の様に連ねて書くと動かない。根本的な解決は next-opti v3 で解決する模様。

2 つを同時に動かすサンプルコード

undefined
1
export default function () {
2
const multiWebp = require(`../../public/cat1200x.jpg?resize&sizes:[640,960,1200,1800]&format=webp`)
3
return (
4
<img
5
srcSet={multiWebp.srcSet}
6
src={multiWebp.src}
7
width={multiWebp.width}
8
height={multiWebp.height} />
9
)
10
}
Image from Gyazo
自分の場合

next.Config.js のなかで、responsive:{sizes: [640, 960, 1200, 1800],} としてあるので component を作って利用している。

src/components/general/OptimizedImages.tsx
1
export function OptimizedImages({ src, alt, imgStyle }) {
2
const multi = require(`@public/assets/${src}?resize`)
3
const multiWebp = require(`@public/assets/${src}?resize&format=webp`)
4
5
return (
6
<React.Fragment>
7
<picture>
8
<source srcSet={responsiveImageWebp.srcSet} type='image/webp' />
9
<img
10
src={responsiveImage.src}
11
srcSet={responsiveImage.srcSet}
12
width={responsiveImage.width}
13
height={responsiveImage.height}
14
/>
15
</picture>
16
</React.Fragment>
17
)
18
}

Low Qualy Image Placeholder

undefined
1
yarn add lqip-loader
undefined
1
export default function () {
2
return (
3
<img src={require('../../public/shirase.jpg?lqip')} />
4
<img src={require('../../public/shirase.jpg')} />
5
)
6
}
Image from Gyazo

lqip(左)の方は 10×7px の 925b に縮小されている。更に filter:blur(10px) 辺りを掛けると更に良さそう。

lqip-loaderを使ったprogressive image loading の実装

medium 風の画像表示をやってみる。まずは useState を使って、lazy load の画像が load されたら、lqpi の opacity を 0 にする。

undefined
1
import React, { useState } from 'react'
2
3
export default function () {
4
const [imageLoaded, setImageLoaded] = useState(false)
5
return (
6
<>
7
<div>
8
<img src={require(`../../public/shirase.jpg?lqip`)}
9
className='lqip' style={{opacity: imageLoaded ? 0 : 1}}
10
/>
11
<img src={require(`../../public/shirase.jpg`)}
12
onLoad={() => setImageLoaded(true)} loading='lazy'
13
/>
14
</div>
15
<style jsx>{`
16
div {
17
position: relative;
18
}
19
20
img {
21
width: 50%;
22
height: auto;
23
}
24
.lqip {
25
position: absolute;
26
top: 0;
27
left: 0;
28
z-index: 10;
29
filter: blur(10px);
30
transition: opacity 500ms cubic-bezier(0.4, 0, 1, 1);
31
}
32
`}</style>
33
</>
34
)
35
}

references