Next.jsにおけるSVGファイルの表示方法を考える
はじめに
フロントエンド開発を進める際に、Next.jsでのSVGでの扱い方について考える機会があったので、Zennで共有しようと思った次第です。
本来、Webサイト内でSVGファイルを表示させる方法は、大きく分けて以下3点だと思われます。
- imgタグの
src
にSVGファイルへのパスを設定して読み込ませる。 - CSSファイル内
のbackground-image
にパスを設定して読み込ませる。 - HTML内でSVGをインラインコードで埋め込む。
※ 細かく話すとSVG以外の拡張子の画像でもインライン化できたり、CSSでもSVGをインラインで読み込ますことが可能ですが、今回は割愛させてください🙏
1と2の方法だと、他の画像ファイル同様、簡単に読み込ませることができますが、SVG内のスタイルをHTMLもしくはCSSで変更できません・・・
3の方法だと、HTMLやCSS、JavaScriptによるスタイルの変更が可能になります🙆♂️
外部ファイルへのHTTPリクエスト回数を減らせるメリットもあることから、SVGはインラインで埋め込むことを推奨したいです。img
タグのように遅延(非同期)で読み込むためのプロパティを容易に設定できない課題はありますが、Next.jsだとDynamic Import等を利用すれば回避できます。
ただし、Next.js(React)内でSVGのインライン埋め込みを実現するには、SVGファイル内のコードをJSX記法に対応させる修正が必要になります。容易に変換してくれるWebサイトやプラグインはありますが、SVGファイルが増えるごとにコードを貼り付けたり、npm scriptを毎度実行する必要があるので、多少手間がかかります🧐
そこで、SVGファイルをReactコンポーネントとして直接importできるように、以下のセットアップを進めます。
セットアップ
-
@svgr/webpack
をインストールします。
yarn add -D @svgr/webpack
-
next.config.js
に以下記述を追加します。
const nextConfig = {
~中略~
+ webpack: (config) => {
+ config.module.rules.push({
+ test: /\.svg$/,
+ use: [
+ {
+ loader: "@svgr/webpack",
+ },
+ ],
+ });
+ return config;
+ },
+ images: {
+ disableStaticImages: true, // importした画像の型定義設定を無効にする
+ },
};
本設定を行うことで、SVGファイルをReactコンポーネントとしてimportできるようになります。また、SVGファイル内のインラインコードの修正も、Next.jsの開発環境におけるホットリロードの対象になるので、開発効率も向上するはずです。
※ babel-plugin-inline-react-svg
というプラグインを導入することでもSVGを呼び出せるように設定することは可能なのですが、SVG内の変更を検知できないため、修正するたびに_next
のディレクトリの削除が必要になり面倒でした。また、ES2020から利用できるようになったDynamic Importの機能を利用する意味でも@svgr/webpack
を導入することをオススメします👍
また、disableStaticImages: true
を設定することで、Next.jsでデフォルトで定義されている画像の型定義設定を無効にできます。無効にすることで、次に行うSVGの型定義を有効化できます。
- 任意の場所に
index.d.ts
を作成し、SVGの型定義を行います。
declare module '*.svg' {
const content: React.FC<React.SVGProps<SVGElement>>;
export default content;
}
※ Next.jsを導入すると、デフォルトでtsconfig.json
内に"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
が設定されているため、上記ファイルをどこに配置しても問題ないですが、今回はsrcにtypesディレクトリを作成し、その中に配置しました。
- (上記設定がすぐ反映されないことが稀にあるので)VsCodeを再起動します。
実際にSVGをReactコンポーネントとして呼び出してみる
上記設定が終わったら、実際にSVGをコンポーネントとして呼び出してみましょう。
今回は例としてIconView
というコンポーネントの中でSVGファイルをimportします。SVG自体は他の画像ファイルと同じく、publicディレクトリに配置しています。
import Icon from "public/images/icon.svg";
export const IconView = () => {
return (
<Icon />
);
};
おそらく、想定通りに表示できるはずです。
ちなみに下記のようにSVGタグに本来設定できるプロパティをpropsとして渡すことが可能です。
import Icon from "public/images/icon.svg";
export const IconView = () => {
return (
- <Icon />
+ <Icon
+ width={10}
+ height={10}
+ stroke={"#fff"}
+ strokeWidth={"2.5px"}
+ fill={"none"}
+ style={{ display: "block"}}
/>
);
};
とても便利にSVGを扱うことが可能になりました✨
TypeScriptを利用しているなら予測補完も効くので、開発体験も良き良きです。
SVGファイルはsvg
タグの他にg
やpath
のようなHTMLタグで構成されているはずですが、それぞれで指定されている(SVGファイル内の)stroke
やfill
等の値を事前に削除しておけば、上記のようなpropsの設定が優先されるはずです。
importされた際にSVGファイル内のコードが変わってしまう問題
今回導入した@svgr/webpack
に組み込まれているsvgo
という最適化ツールがデフォルトだと有効になってしまっていることが原因で上記の問題が発生し、(線が一部表示されないなど)SVGが想定通りに表示されないことがありました。
そこで、next.config.js
に以下記述を追加します。
const nextConfig = {
~中略~
webpack: (config) => {
config.module.rules.push({
test: /\.svg$/,
use: [
{
loader: "@svgr/webpack",
+ options: {
+ svgo: false, // 圧縮無効
+ },
},
],
});
return config;
},
~中略〜
};
これでsvgo
が無効になり、SVGファイル内のコードが勝手に変換されなくなったはずです🌞
svgo
はインラインコードを自動で圧縮してくれるので便利ですが、上記の問題が発生することがあるので、利用時には注意が必要ですね。
Storybookへの対応
StorybookでもSVGをコンポーネントとして扱うためには、.storybook/main.js
に以下記述を追加してください。
~前略~
module.exports = {
~中略~
webpackFinal: async (config, { configType }) => {
+ config.resolve.modules = [...(config.resolve.modules || []), path.resolve("./")]; // 絶対パスでimportできるようにする
+ config.module.rules.push({
+ test: /\.svg$/,
+ use: [
+ {
+ loader: "@svgr/webpack",
+ options: {
+ svgo: false, // 圧縮無効
+ },
+ },
+ ],
+ });
+ const fileLoaderRule = config.module.rules.find((rule) => rule.test && rule.test.test(".svg"));
+ fileLoaderRule.exclude = /\.svg$/;
+ return config;
},
};
※上記記述はWebpack5の利用を想定
ここまで完了すれば、SVGをインラインで便利に読み込めるようになったはずです!
続きはこちら・・・
Discussion
@svgr/webpack
っていうライブラリあるんですね!知らなかったので勉強になりました!このコメントなのですが、 Core Web Vitals の観点で SVG をインラインにした方が良いというのは場合によりけりかと思いました!
例えば巨大な SVG をインラインに埋め込むとメモリ喰ったり初期描画の負担にもなりえますし、遅延的に SVG を表示したいケースではブラウザネイティブに
<img src='test.svg' loading='async' />
とかにした方が良いかなと。ついでですが、コード例にある
React.Fragment
(<></>
)も不要かなと思いました!コメントありがとうございます🙌
上記を考慮した表現ができておりませんでした・・・
該当箇所を修正しました。ご指摘感謝です🙆♀️
実は現在、Dynamic Importを使ってSVGコンポーネントを非同期に読み込ませるための記事を執筆中なので、身が引き締まる次第です🔥
こちらも指摘ありがとうございます!うっかり消し忘れておりました🙇♀️