31
loading...
This website collects cookies to deliver better user experience
ImageEngine
’s distribution, but is applicable to any other situation where you need to customise the loaders. We’ll be building a grid layout image portfolio with the option to view the images in a full window lightbox. You can check the app we’ll be building here and the github repo.Next.js
AppCustom Loaders
for use by our Next.js Image
componentsImageEngine
and set our vercel environment to use itNext.js
<Image/>
component is a pretty neat tool that is available in any Next.js
project you might start. It allows one to set up a system of breakpoints, sizes and settings to apply to our image assets. It takes care of preventing Cumulative Layout Shift on page rendering when provided with the correct size information and optimize our assets according to buckets of resolution/viewport/image sizes. Vercel
’s hosting, the performance of initial optimization is not as fast and can take a bit of time. If you’re not doing SSR
and instead are deploying a static website build, as of now, you won’t be able to use their Image
component at all.width
to determine the optimized images. So if you have a reasonable mix of vertical and horizontally oriented images then it will be much harder to serve the minimum needed sizes for those vertical images. Vercel.com
then you might be interested in something that allows you to optimize your images up to the single pixel count, doesn’t rely on your server performance nor caching solutions, and offers very fast CDN distributions to deliver the best possible loading and viewing experience to your customers and users.Image
loader for Next.js
and some tricks for layout responsiveness.npx create-next-app
npm i @imageengine/react
npm run dev
http://localhost:3000
index.js
page has and replace it with:import Head from "next/head";
export default function Home() {
return (
<div className="main-container">
<Head>
<title>ImageEngine Optimized Lightbox</title>
<meta name="description" content="Next.js custom image loaders leveraging ImageEngine CDN for highly optimized images" />
<link rel="icon" href="/favicon.ico" />
</Head>
</div>
);
};
/pages/_app.js
to the followingimport Head from "next/head";
import "../styles/globals.css";
function IECustomLoader({ Component, pageProps }) {
return (
<>
<Head>
<link rel="icon" type="image/png" href="/favicon.png"/>
<meta name="viewport" content="initial-scale=1.0" />
<meta name="description" content="Learn how to use ImageEngine with Nextjs custom loaders to serve highly optimised image assets from your CDN to your users." />
<meta property="og:title" content="ImageEngine with NextJS" />
<meta property="og:description" content="Learn how to use ImageEngine with Nextjs custom loaders to serve highly optimised image assets from your CDN to your users." />
</Head>
<Component {...pageProps} />
</>
);
};
export default IECustomLoader;
next.config.js
on the root of our project and inside it:module.exports = {
env: {
DISTRIBUTION: process.env.DISTRIBUTION
},
images: {
deviceSizes: [1920, 1500, 1000, 500, 300],
imageSizes: []
}
};
pages/api
folder, styles/Home.module.css
and public/vercel.svg
::rm -rf pages/api
rm styles/Home.module.css
rm public/vercel.svg
components
, and inside the public
folder create a ie-loader-images
folder....
pages /
_app.js
index.js
styles /
global.css
components /
public /
favicon.ico
ie-loader-images
/public/ie-loader-images/
.thumbnail
component and a lightbox
component. Both components will receive a src
and alt
props for the image, an onClick
function and the sizes of the viewport
. The lightbox
component will additionally receive two functions for moving to the next and previous image.Next.js
Image
component fixes the size of the image to prevent Cumulative Layout Shift
, but since we establish fixed size containers in CSS, that would have been addressed as well. The other interesting bit there is the small CSS trickery to get the hover
effect to display the labels without JS. The remaining is basic styling.components/thumbnail.js
with:import Image from "next/image";
import { constructUrl } from "@imageengine/react/build/utils/index.js";
import { useState, useEffect, createRef } from "react";
function thumbnail_loader({ src, quality, distribution, width }) {
let url = distribution + src,
directives = {
width: width,
height: width,
fitMethod: "cropbox",
compression: 100 - quality,
sharpness: 10
};
return constructUrl(url, directives);
};
export default function Thumbnail({ onClick, src, alt, window_sizes }) {
let thumbnail_ref = createRef(),
[width, set_width] = useState(null),
[initial_size, set_initial_size] = useState(null);
useEffect(() => {
if (window_sizes) {
let dimensions = thumbnail_ref.current.getBoundingClientRect(),
n_width = Math.ceil(dimensions.width);
if (!initial_size) { set_initial_size(n_width); }
if (initial_size >= n_width) { set_width(initial_size); }
else {
set_initial_size(n_width);
set_width(n_width);
}
}
}, [window_sizes]);
return (
<div className="image-thumbnail" onClick={onClick} ref={thumbnail_ref}>
{width ?
<Image
src={src}
alt={alt}
sizes={`(max-width: 320px) 300px, (max-width: 500px) 500px, (max-width: 1000) 1000px, (max-width: 1500) 1500px, ${width}px`}
layout="responsive"
objectFit="cover"
objectPosition="center"
width={width}
height={width}
loader={process.env.DISTRIBUTION ? (args) => thumbnail_loader({...args, distribution: process.env.DISTRIBUTION}) : undefined}
quality={80}/> : null
}
<div className="image-details">
<p>{alt}</p>
</div>
</div>
);
};
onClick, src, alt, window_sizes
. We create a DOM ref
that we can use to get the actual HTML element of our wrapper, and we set two useState
’s, one to store the current width, and another the store the initial width. Most times these will be the same. useEffect
with a dependency on the value of window_sizes
. Meaning this will run on mount once and then any time the prop window_sizes
changes. Inside this hook if the window_sizes
has a value, we use the ref
for getting the wrapper component dimensions, we round the value, set the initial_size
if it’s not set yet, and, then, if the new width is bigger than the the initial_size
we set both initial
and width
to this new size, and if not we keep the initial_size
and set it as our width
value. onClick
set to the handler passed down as a prop (that will open the lightbox
) and the ref
we created, so that we can use the ref
to get the wrapper width.Next.js
Image
component, and we set src
, alt
, sizes
(for the breakpoints), layout
as responsive (so that it fetches new images if the width and sizes change), objectFit
and objectPosition
so that it forces the image to be displayed covering the full “square” and centered, quality
, and in case there’s an environment variable DISTRIBUTION
set, we use the specific loader, an anonymous function that calls the thumbnails_loader
we defined at the top of the file with an additional argument, thedistribution
url, otherwise we set it as null so the default one will be used.DISTRIBUTION
is set that loader will be used to provide the actual url
the Image
component will use. By default the loader has access to 3 params provided by Next.js
, the original src
, quality
value and width
. In this case since it’s a square image, the width
is enough, but we still need the distribution
url, so that we can generate the right source for our image.constructUrl
provided by @imageengine/react we can pass an object with properties that dictate how the CDN will provide the images. Here we pass width
, height
, fitMethod
, compression
(it’s the inverse scale from the quality) and sharpness
to apply a small amount of sharpness to the final image.constructUrl
isn’t directly exported by the package - which means that although probably unlikely, it’s place in the lib might change - nonetheless it will probably be made public in a future release, but otherwise, you can always either copy the specific logic for it, or use directives directly and build your own url.srcset sizes
entries, and that is fine since the url codifies the needed settings itself, but when using ImageEngine
we don’t really need to rely on srcset
since we can generate exactly the sizes we want on-the-fly.lightbox
component, create the file components/lightbox.js
:import Image from "next/image";
import { constructUrl } from "@imageengine/react/build/utils/index.js";
function lightbox_loader({ src, quality, distribution, w, h }) {
let url = distribution + src,
directives = {
width: w,
height: h,
fitMethod: "box",
compression: 100 - quality
};
return constructUrl(url, directives);
};
export default function Lighbox({ onClick, src, alt, window_sizes, previous, next }) {
return (
<div className="lightbox" onClick={onClick}>
<button className="previous" onClick={previous}/>
<div className="image-lightbox">
{window_sizes ? <Image
src={src}
alt={alt}
sizes={`(max-width: 320px) 300px, (max-width: 500px) 500px, (max-width: 1000) 1000px, (max-width: 1500) 1500px, ${window_sizes.w}px`}
objectFit="contain"
objectPosition="center"
width={window_sizes.w}
height={window_sizes.h}
loader={process.env.DISTRIBUTION ? (args) => lightbox_loader({...args, ...window_sizes, distribution: process.env.DISTRIBUTION}) : undefined}
quality={90}
/> : null}
</div>
<button className="next" onClick={next}/>
</div>
);
};
w
and h
values directly from the window_sizes
prop, and we use a different objectFit
and quality
.width
and height
to our loader is that since we want the image to fit the viewport, with both those values and the box
fit, ImageEngine
will be able to generate perfectly sized images for them, especially when they’re vertical.ImageEngine
can provide better optimisations and the reason is related to how the Next.js
Image
component works. deviceSizes: [1920, 1500, 1000, 500, 300]
on our next.config.js
what we’re saying is, we’ll have 5 buckets of width sizes we want to allow. By using the sizes
property on the Image
component we can make it so that given a certain width
the browser will try to match it with the smallest possible srcset
that covers that width
. For horizontal or square images this works fine, as there’s a direct match between the max width and the bucket. But when the image is vertical, this no longer matches neatly. So if you open the lightbox with a vertical image and the viewport size is 1800px
wide, but only 750px
high, the image that will be retrieved will be the one matching the 1920
entry. This is actually a bigger image since it’s vertical, it’s width to match 1920
will make it quite bigger than it needs to be. ImageEngine
, in the same situation, with a fitMethod of box, ImageEngine
’s engine will actually be smart enough to see that the image needs to be 750px
high at most, and will resize it by that axis. So the result might be a 400px X 750px
image, instead of 1920px X 3000px
for instance. Next.js
optimizer, by doing ourselves the calculations of what width would correspond to that maximum height - but since this isn’t normally available we usually can’t. Plus, that would need to change whenever we had changes on layout, styling, or on the source images.ImageEngine
we don’t need to worry about that, since it’s always going to be the perfect fit for the dimensions we give it. Plus, in the case of the thumbnails, if we changed the size of the wrappers in CSS, it would still work automatically, whereas with the default Image
component we might need to change our deviceSizes
or do small adjustments. We could even make the thumbnail
element be completely future proof if instead of providing only the width
(as it was a square) we also provided the height
of the wrapper, so that even if we changed the styling to a different ratio it would still work correctly. Since it’s a demo, we’re using only one side but there’s no reason to not use both as we already have the DOM element from which we can extract the height as well.index.js
file.pages/index.js
:import Head from "next/head";
import Thumbnail from "../components/thumbnail.js";
import Lightbox from "../components/lightbox.js";
import { useEffect, useState } from "react";
const IMAGES = [
["/ie-loader-images/h-lightbox-1.jpeg", "Harvested field with hay bales - Alentejo, Portugal © Micael Nussbaumer"],
["/ie-loader-images/h-lightbox-2.jpeg", "Family cycling and skating in abandoned Tempelhof Airport lane - Berlin, Germany © Micael Nussbaumer"],
["/ie-loader-images/h-lightbox-8.jpeg", "Group of hindu and muslim kids posing for a photo - New Delhi, India © Micael Nussbaumer"],
["/ie-loader-images/v-lightbox-4.jpeg", "Buddhist Monk Portrait with a statue of the buddhist mythological Seven Headed Naga serving as background - Siem Reap, Cambodia © Micael Nussbaumer"],
["/ie-loader-images/h-lightbox-5.jpeg", "Traditional Nepalase Hindu Temple in one of the many lively city squares of Kathmandu, Nepal © Micael Nussbaumer"],
["/ie-loader-images/v-lightbox-6.jpeg", "Woman in traditional Nepalese clothing sitting in a valley in Pokhara, Nepal © Micael Nussbaumer"],
["/ie-loader-images/h-lightbox-7.jpeg", "Geometric pattern on a ceiling inside the Red Fort - New Delhi, India © Micael Nussbaumer"],
["/ie-loader-images/h-lightbox-3.jpeg", "Kids silhuetes in the sea near-shore close to sunset - Phu Quoc Island, Vietnam © Micael Nussbaumer"],
["/ie-loader-images/h-lightbox-9.jpeg", "Portrait of four man sitting during a pause in their badminton game - Hanoi, Vietnam © Micael Nussbaumer"]
];
export default function Home() {
let [opened, set_opened] = useState(false);
let [window_sizes, set_window_sizes] = useState(null);
let resize_timer;
const get_window_sizes = () => {
let doc = document.documentElement;
return {w: doc.clientWidth, h: doc.clientHeight};
};
const previous = (evt) => {
evt.stopPropagation();
if (opened > 0) { set_opened(opened - 1); }
else { set_opened(IMAGES.length - 1); }
};
const next = (evt) => {
evt.stopPropagation();
if (opened < (IMAGES.length - 1)) { set_opened(opened + 1); }
else { set_opened(0); }
};
const set_timing = () => {
if (resize_timer) { clearTimeout(resize_timer); }
resize_timer = setTimeout(
() => {
set_window_sizes(get_window_sizes());
resize_timer = null;
}, 2000
);
};
useEffect(() => {
window.addEventListener("resize", set_timing);
set_window_sizes(get_window_sizes());
return () => window.removeEventListener("resize", set_timing);
}, []);
return (
<div className={`main-container ${opened || opened === 0 ? "no-overflow" : ""}`} >
<Head>
<title>ImageEngine Optimized Lightbox</title>
<meta name="description" content="Next.js custom image loaders leveraging ImageEngine CDN for highly optimized images" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
{IMAGES.map(([path, alt], index) => {
return <Thumbnail key={`image-thumb-${index}`} src={path} alt={alt} onClick={(_evt) => set_opened(index)} window_sizes={window_sizes}/> ;
})
}
</main>
{opened || opened === 0 ? <Lightbox src={IMAGES[opened][0]} alt={IMAGES[opened][1]} onClick={(_evt) => set_opened(null)} window_sizes={window_sizes} previous={previous} next={next}/> : null}
<footer>
<a href="https://imageengine.io" target="_blank">ImageEngine</a>
<a href="https://micaelnussbaumer.com" target="_blank">© Micael Nussbaumer 2021</a>
<a href="https://nextjs.org" target="_blank">Next.js</a>
<a href="https://vercel.com" target="_blank">Vercel</a>
</footer>
</div>
);
};
index.js
file we define an IMAGES
array, that contains the images we’ll show. The contents are mostly straightforward. We use useState
to set both the window size and if the lightbox is open. We define functions for getting the window size, for moving forward and backwards across the images, and to control any possible resizing of the viewport.useEffect
to set a listener on the window resize
event and we set the original window sizes. Although we only have one page in our Next.js
app, we also set a cleanup function on the useEffect
by returning an anonymous function to remove the eventListener
we added previously. This is just good form, because if you’re using React as a SPA with multiple pages and you don’t clean-up your useEffects, you might end with memory-leaks or multiple listeners being triggered (and most likely throwing exceptions since what they’ll be referring to won’t be around anymore).resize
event we use a setTimeout
and a control variable resize_timer
. This is so that we don’t trigger multiple window_sizes
changes - as that would trickle down to our components and trigger multiple fetches for different image sources as the dimensions changed, for instance if resizing the window on a desktop manually. At the same time, listening to resize events and updating the window_sizes
takes care of refetching the correct size in case a user is seeing the website on a mobile and changes the display from vertical to horizontal for instance. JSX
contents are pretty regular, we map our IMAGES
array into Thumbnail
’s components passing src
, alt
, window_sizes
and a onClick
handle.opened
state is updated to the index of that image in our array and we use that to both display our Lightbox
component and to read the correct data for our image. On the Lightbox
we pass the same props plus the previous
and next
functions.footer
items.vercel
, by using their CLI from our project’s root folder executing:vercel
vercel --prod
ImageEngine
distribution.Vercel
dashboard for our project, or through the CLI
when deploying:vercel --build-env DISTRIBUTION=”https://our_ie_address.imgeng.in” --prod
ImageEngine
distribution for the images!Next.js
’s Image
component along with vercel
’s automated infrastructure is really a great improvement over using non-optimized assets and will work well for many types of websites, there’s a few cases where ImageEngine is a better option overall. Next.js
but not deploying on vercel
then you’ll probably want to use it too - the Image
component works the best possible in vercel
as its infrastructure is prepared to handle it specifically (they’re the authors of Next.js
) but if you’re deploying on your own server then it’s a different matter and having it stored, prepared and cached over a very fast external CDN will help you achieve the best performance for your website. 31