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

 / #nextjs

※ 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

yarn add next-optimized-images

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

yarn add npm imagemin-mozjpeg imagemin-optipng

Config

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

const withPlugins = require('next-compose-plugins')
const optimizedImages = require('next-optimized-images')
const nextOptimizedImagesConfig = {
imagesFolder: 'images',
imagesName: '[name]-[hash].[ext]',
handleImages: ['jpeg', 'png', 'webp'],
removeOriginalExtension: true,
optimizeImages: process.env.MODE_ENV !== 'development',
optimizeImagesInDev: false,
mozjpeg: { quality: 85, },
optipng: { optimizationLevel: 3, },
webp: { preset: 'default', quality: 85,},
}
module.exports = withPlugins(
[
[ optimizedImages, nextOptimizedImagesConfig ],
],
)

usage

href (image path)

画像は<img src={require(../../example.jpg)} />の様に指定するが、md 内で指定するときなどは面倒なので、next.js.configにパスのエイリアスを追加する。

Image from Gyazo

原因は webpack にある模様

const { resolve } = require('path')
const nextConfig = {
webpack: (config) => {
config.resolve.alias['@public/assets'] = resolve(__dirname, 'public/assets')
return config
},
}
// ...
module.exports = withPlugins([ ... ], nextConfig)

convert to webp

yarn add webp-loader

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

export default () => (
<picture>
<source srcSet={require('./images/my-image.jpg?webp')} type="image/webp" />
<img src={require('./images/my-image.jpg')} />
</picture>
)

responsive image

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 として指定できる。

// ...
const nextOptimizedImagesConfig = {
// ...
responsive: {
adapter: require('responsive-loader/sharp'),
sizes: [640, 960, 1200, 1800],
disable: process.env.MODE_ENV === 'development'
},
// ...
}
module.exports = withPlugins(
[[ optimizedImages, nextOptimizedImagesConfig ],],
nextConfig
)

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

// どれでも動く
const multi = require(`../../public/cat1200x.jpg?resize&sizes:[640,960,1200,1800]`)
// const multi = require('../../public/cat1200x.jpg?resize&sizes[]=640&sizes[]=960&sizes[]=1200&sizes=[1800]')
// const multi = require('../../public/cat1200x.jpg?resize&sizes[]=640,sizes[]=960,sizes[]=1200,sizes[]=1900')
export default () => (
<img
srcSet={multi.srcSet}
src={multi.src}
width={multi.width}
height={multi.height}/>
);

Image from Gyazo

webp-loader と responsive-loader

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

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

export default function () {
const multiWebp = require(`../../public/cat1200x.jpg?resize&sizes:[640,960,1200,1800]&format=webp`)
return (
<img
srcSet={multiWebp.srcSet}
src={multiWebp.src}
width={multiWebp.width}
height={multiWebp.height} />
)
}

Image from Gyazo

自分の場合

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

export function OptimizedImages({ src, alt, imgStyle }) {
const multi = require(`@public/assets/${src}?resize`)
const multiWebp = require(`@public/assets/${src}?resize&format=webp`)
return (
<React.Fragment>
<picture>
<source srcSet={responsiveImageWebp.srcSet} type='image/webp' />
<img
src={responsiveImage.src}
srcSet={responsiveImage.srcSet}
width={responsiveImage.width}
height={responsiveImage.height}
/>
</picture>
</React.Fragment>
)
}

Low Qualy Image Placeholder

yarn add lqip-loader
export default function () {
return (
<img src={require('../../public/shirase.jpg?lqip')} />
<img src={require('../../public/shirase.jpg')} />
)
}

Image from Gyazo

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

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

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

import React, { useState } from 'react'
export default function () {
const [imageLoaded, setImageLoaded] = useState(false)
return (
<>
<div>
<img src={require(`../../public/shirase.jpg?lqip`)}
className='lqip' style={{opacity: imageLoaded ? 0 : 1}}
/>
<img src={require(`../../public/shirase.jpg`)}
onLoad={() => setImageLoaded(true)} loading='lazy'
/>
</div>
<style jsx>{`
div {
position: relative;
}
img {
width: 50%;
height: auto;
}
.lqip {
position: absolute;
top: 0;
left: 0;
z-index: 10;
filter: blur(10px);
transition: opacity 500ms cubic-bezier(0.4, 0, 1, 1);
}
`}</style>
</>
)
}

references