New package, who dis?
A webpack plugin to automagically bring your website images to a whole new level of responsiveness! This plugin derives from our previous attempt to solve the same problem using only a webpack loader. If you were using it, check out the migration guide.
This plugin tackles in an unified way three main problems with images on the web nowadays:
- intelligent images transformation based on focal points of an image (art direction);
- images resizing to always serve the lightest bundle possible (resolution switching);
- usage of most efficient image formats (automatic conversion).
Moreover, we aim to automatize everything that doesn't strictly require your input:
- calculating best breakpoints for resolution switching;
- ordering sources by most efficient image format;
- providing sensible defaults;
- serving a fallback for older browsers;
- and more!
We also focused on flexiblity and customizability: transformation, resizing and conversion engines can be easily switched with your implementation, which you can then PR here and make available to others.
Aren't there other tools doing the same stuff? We found some notable tools while evaluating if it was worth to create our own package, but none of them combines all the requirements we now offer:
- manages together art direction, resolution switching and conversion, with all their weird interactions;
- process images both when used via
<img>
tags andbackground-image
CSS rules; - framework agnostic;
- operates at build time (useful for SSG builds);
- works offline;
- free;
- open source;
- customizable and flexible at its core.
For more info, check out the issue from which this package spawned.
- Roadmap
- Donations and shameless self-advertisement
- Installation
- Migration
- Usage
- Configuration
- Caveats & FAQ
- Contributing
- License
Features we'd like to implement, by most-wanted order.
- Add PNG to supported formats
- Define defaults for arbitrary groups of images
- Write more granular unit tests
- Add TSDocs to public methods
- Support Cloudinary adapter for transformer, resizer and converter
- Support video conversion and processing
- Test with HMR
- Pass-through custom configuration to underlying engines
Dreamonkey is a software house based in Reggio Emilia, Italy. We release packages as open-source when we feel they could benefit the entire community, nontheless we spend a considerabile amount of time studying, coding, maintaining and enhancing them.
Does your business or personal projects depend on our packages, or you need a particular feature implemented which would benefit the whole community? Consider donating here on Github to help us maintain and develop them, and allow us to create new ones!
Do you need a UX and quality driven team to work on your project? Get in touch with us through our incredibly elaborate quotation request page or our much less cool contact form and let's find out if we are the right choice for you!
Install via
yarn add -D @dreamonkey/responsive-image-plugin
or
npm install -D @dreamonkey/responsive-image-plugin
.
Add the plugin into your webpack config.
const ResponsiveImagePlugin =
require('@dreamonkey/responsive-image-plugin').default;
webpackConf.plugins.push(
new ResponsiveImagePlugin({
/* ... */
}),
);
webpackConf.module.rules.push({
test: /\.html$/,
loader: ResponsiveImagePlugin.loader,
});
If you plan to process CSS background images, you should also include the package as you'd do with a polyfill.
webpackConf.entry['responsive-bg-image-handler'] =
ResponsiveImagePlugin.bgHandler;
<!--
NB: `src` attribute value could change dependending on your webpack `output.filename` (https://webpack.js.org/configuration/output/#outputfilename) and `output` configuration, you're not bound to ``
-->
<script src="./responsive-bg-image-handler.js">
Presumely due to some kind of incompatibility with theirs HTML loader, you must tap into low level Vue template to use this plugin with Quasar framework (on which it has been tested and developed).
const ResponsiveImagePlugin =
require('@dreamonkey/responsive-image-plugin').default;
webpackConf.plugins.push(
new ResponsiveImagePlugin({
/* Quasar output folder */
outputDir: '/img/',
/* ... */
}),
);
webpackConf.module.rules.push({
test: /\.vue$/,
resourceQuery: /type=template/,
loader: ResponsiveImagePlugin.loader,
});
It is not possible to specify only test: /\.vue$/
because Vue templates are actually processed many times (one for general file plus one per each used tag) and this would break the loader workflow.
A caching mechanism (as suggested by Vue creator in this cases) won't work efficiently and will break framework-agnosticism.
If you plan to process CSS background images, you should also include the package as you'd do with a polyfill.
webpackConf.entry['responsive-bg-image-handler'] =
ResponsiveImagePlugin.bgHandler;
You don't need to manually include it via a script
tag as Quasar already does it automatically for every entry
property.
Set webpackConfig.infrastructureLogging = { level: 'log' }
to see resources processing logs in real-time.
Art Direction, Resolution Switching and Conversion, are powered via fully decoupled adapters which allow to swap engine as you please. Every engine has its installation guide (independent from this plugin) and you can also provide your custom adapter to support a new engine (in which case, we welcome PRs!)
thumbor
(art direction)
First setup Docker on your system:
- Linux (we suggest to use "rootless" mode)
- Windows
- Mac
Then pull docker Thumbor image running docker pull minimalcompact/thumbor
.
This engine ships with a preset configuration.
Due to its nature of spawning a brand new container for every build cycle, using thumbor
will not leverage Thumbor built-in cache mechanism, meaning build time will not decrease on subsequent runs.
sharp
(resolution switching | conversion)
Everything should "Just Work™" out-of-the-box. It's installed by default when adding the plugin dependency, but check for libvips
dependency if something doesn't work properly. If you get build errors at the first run, try refreshing your lock file and node_modules
folder.
If you're coming from previous iteration of this project, there are some important changes to be aware of:
paths.aliases
has been removed, the plugin now resolves paths using the same aliases defined into webpack, plus vue-loader~
prefix marker for module requests (which is stripped away);paths.outputDir
has been moved one level higher, into the global configuration;- options should now be provided to the plugin when creating a new instance, instead of the loader;
- metadata generation is now decoupled from actual image generation, this means that adapters now:
- can be written as lambda functions;
- should only care about generating the actual image data and return it as a
Buffer
.
Add responsive
attribute over an <img>
component and it will be enhanced with conversion and resolution switching!
<img responsive src="my-little-calogero.jpg" />
By default all classes on <img>
will also be copied over to the wrapping <picture>
.
If you want to change classes which are applied to <img>
after the rewrite took place, you can use responsive-img-class
attribute.
If you want to manually specify which classes should be applied to <picture>
, you can use responsive-picture-class
attribute.
If you add either responsive-img-class
or responsive-picture-class
without any value or with an empty value, classes on <img>
and <picture>
will be erased.
<img class="hello there" responsive src="my-little-calogero.jpg" />
<!-- WILL BECOME -->
<picture class="hello there">
<source />
<source />
<!-- ... -->
<img class="hello there" responsive src="something.jpg" />
</picture>
<img
class="hello there"
responsive
responsive-img-class="master kenobi"
src="my-little-calogero.jpg"
/>
<!-- WILL BECOME -->
<picture class="hello there">
<source />
<source />
<!-- ... -->
<img class="master kenobi" responsive src="something.jpg" />
</picture>
<img
class="hello there"
responsive
responsive-img-class
src="my-little-calogero.jpg"
/>
<!-- WILL BECOME -->
<picture class="hello there">
<source />
<source />
<!-- ... -->
<img responsive src="something.jpg" />
</picture>
You can opt-in to art direction adding responsive-ad
attribute. You can also provide an encoded inline transformation as the attribute value which will be merged on top of default transformations.
This allow to overwrite size or ratio of an existing transformation on a single image.
The syntax for inline transformations is:
- it can contain one or more properties;
- each property definition starts with the property name (
ratio
,path
,size
, etc.) followed by an equality sign (=
) and one or more options separated by a comma (,
); - every option is composed by a value and, optionally, one or more viewports to which it must be applied;
- wiewports must be enclosed into curly braces (
{}
) and separated by a pipe char (|
).
Adding a responsive-ad-ignore
attribute without value will disable all default transformations, while providing a pipe-separated list of transformation names will disable only the selected ones.
Notice that you can use both a viewport width or an alias to reference a transformation in the value of both attributes.
<!-- Opt-in to art direction -->
<img responsive responsive-ad src="my-little-nicola.jpg" />
<!--
Define inline transformations:
- the first uses a viewport width as name and explicitly define `ratio` and `size`.
- the second uses an alias as name and define a custom image
(it will be used "as-is"); `size` has not been specified and
will be inferred from the default size.
-->
<img
responsive="size=0.5{699}"
responsive-ad="ratio=3:2{699};path=./custom_example.jpg{md}"
src="my-little-francisco.jpg"
/>
<!--
Define inline transformations:
- on `xs` and `md` viewports the `size` is `0.5`, while it's `0.33` on `sm` one. All other viewports will use the default size.
- on `xs` and `md` viewports the `ratio` is `1:2`, while it's `3:2` on `sm` one. All other viewports will use the default ratio (which is the original image ratio).
-->
<img
responsive="size=0.33{sm},0.5{xs|md}"
responsive-ad="ratio=3:2{sm},1:2{xs|md}"
src="my-little-francisco.jpg"
/>
<!--
Ignore all default transformations and only apply the one specified.
-->
<img
responsive-ad-ignore
responsive-ad="ratio=2:3{1023}"
src="my-little-kappa.jpg"
/>
<!-- Ignore only 'xs' and '1500' transformations, apply all other default ones -->
<img
responsive
responsive-ad-ignore="xs|1500"
responsive-ad
src="my-little-cuenta.jpg"
/>
Add responsive
and responsive-bg
attributes on any tag whose background-image
you want to manage. The latter should be initialized to the path of the source image.
<div class="enhanced-bg-div" responsive responsive-bg="my-little-calogero.jpg">
<p>Hey there, I'm famous</p>
</div>
All conversion, resolution switching and art direction options apply with the same API as if they were used on an <img>
tag.
To keep the same GUI both in development and production mode you should add a fallback background-image
CSS rule (usually with the same value as responsive-bg
attribute) which conditionally target the element when the plugin is not applied. A data-responsive-bg
attribute is added to every enhanced element for this reason.
.enhanced-bg-div:not([data-responsive-bg]) {
background-image: url(my-little-calogero.jpg);
}
Adding a fallback without the :not([data-responsive-bg])
selector will cause the browser to load the un-optimized image anyway, causing harm instead of benefit.
You can check out the default configuration here.
// Full configuration, you won't ever need all this options
const fullOptionsExample: ResponsiveImagePluginConfig = {
outputDir: '/images/',
defaultSize: 1.0,
viewportAliases: {
xs: '699', // 0-699
md: '1439', // 700-1439
},
conversion: {
converter: 'sharp',
enabledFormats: {
webp: true,
jpg: true,
},
},
resolutionSwitching: {
resizer: 'sharp',
breakpoints: {
minViewport: 200,
maxViewport: 3840,
maxSteps: 5,
minStepSize: 35,
},
},
artDirection: {
transformer: 'thumbor',
defaultRatio: 'original',
defaultTransformations: {
xs: { ratio: '4:3' },
md: { ratio: '2:3', size: 0.5 },
},
},
};
// Example of a typical configuration, if using art direction
const options: DeepPartial<ResponsiveImagePluginConfig> = {
outputDir: '/img/',
viewportAliases: {
xs: '699', // 0-699
sm: '1023', // 700-1023
md: '1439', // 1024-1439
lg: '1919', // 1440-1919
xl: '3400', // 1920-3400
},
artDirection: {
transformer: 'thumbor',
defaultTransformations: {
xs: { ratio: '4:3' },
sm: { ratio: '2:1' },
md: { ratio: '2:3' },
lg: { ratio: '16:9' },
xl: { ratio: '21:9' },
},
},
};
If set to true, this plugin instance will assume another plugin instance is in charge of generating and updating sources and it will halt its compilation waiting for generation to complete. Take care when using it as:
- the process will wait undefinitely if no other instance completes the generation process;
- the process will error out if more than one instance is in charge of generating sources.
This option is needed when you need to generate different builds of the same application (eg. SSR which need a client and server version).
const opt = { dryRun: isServer };
Specify a folder which will prefix images uri emitted by this plugin. Your production bundle probably isn't organized with a flat folder structure, so you'll want to use this options most of the time.
// All images will be emitted into the bundle `img` folder
const opt = { outputDir: '/img/' };
Maps of aliases to viewport widths which is used when specifying different sizes for resolution switching or when referencing a transformation.
const opts = {
viewportAliases: {
xs: '699', // 0-699
sm: '1023', // 700-1023
md: '1439', // 1201-1439
lg: '1919', // 1440-1919
xl: '3400', // 1920-3400
},
};
Will be used when applying transformations or creating resolution switching breakpoints.
If provided as a percentage (size <= 1.00
) it's considered as the width size multiplier with respect to the maxViewport.
If provided as a number bigger than 300
it's considered as the width in pixels.
Value is capped to 0.10
on lower bound.
Specify the adapter function to use for image format conversion.
You can provide the name of a preset adapter (only sharp
for now) after you installed it properly on your system.
Providing null
disables conversion.
// Disables conversion
const opt = { converter: null };
// Provide custom adapter
const opt = {
converter: (sourcePath, destinationPath, uriWithoutHash, format) => {
/**/
return convertedImageAsBuffer;
},
};
// Provide custom adapter defined elsewere
const conversionAdapter: ConversionAdapter = (
sourcePath,
destinationPath,
uriWithoutHash,
format,
) => {
/**/
return convertedImageAsBuffer;
};
const opt = { converter: conversionAdapter };
Keys of this object represents available formats (jpg
or webp
), while their value represent their enabled status.
// Only serve webp formats
const opt = { enabledFormats: { webp: true, jpg: false } };
Source will be ordered by format efficiency: webp
> jpg
Breakpoints generation adds as many breakpoints as possible into narrow viewports (smartphones), which suffer high bundle sizes the most (eg. when using data network); it also grants some breakpoints to wider viewports (laptops, desktops), where is less critical to save bandwidth. If narrow viewports need less breakpoints than originally allocated for them, those breakpoints are re-allocated to wider viewports and removed when they cannot be used in the widest viewport available.
Specify the adapter to use for image resizing.
You can provide the name of a preset adapter (only sharp
for now) after you installed it properly on your system.
Providing null
disables resolution switching.
// Disables resolution switching
const opt = { resizer: null };
// Provide custom adapter, **never use a lambda function**
const opt = {
resizer: function (sourcePath, destinationPath, breakpointWidth) {
/**/
return breakpoint;
},
};
// Provide custom adapter defined elsewere, **never use a lambda function**
const resizingAdapter: ResizingAdapter = function (
sourcePath,
destinationPath,
breakpointWidth,
) {
/**/
return breakpoint;
};
const opt = { resizer: resizingAdapter };
The minimum viewport which will be considered when automatically generating breakpoints.
The maximum viewport which will be considered when automatically generating breakpoints.
Maximum number of breakpoints which can be generated, the actual count can be lower due to minSizeDifference
option.
It doesn't include breakpoints generated by art direction transformations.
Minimum size difference (expressed in KB) there should be between a breakpoint and both its preceding and following ones.
Specify the adapter to use for image transformations.
You can provide the name of a preset adapter after you installed it properly on your system.
Providing null
disables art direction.
// Disables art direction
const opt = { transformer: null };
// Provide custom adapter
const opt = {
transformer: function (imagePath, transformations) {
/**/
return transformationSource;
},
};
// Provide custom adapter defined elsewere
const transformationAdapter: TransformationAdapter = (
imagePath,
transformations,
) => {
/**/
return transformationSource;
};
const opt = { transformer: transformationAdapter };
The ratio which will be used when applying transformations, if not explicitly provided.
Map of default transformations.
const opts = {
defaultTransformations: {
xs: { ratio: '4:3' },
sm: { ratio: '2:1' },
md: { ratio: '2:3' },
lg: { ratio: '16:9' },
xl: { ratio: '21:9' },
},
};
NO!
Being a webpack plugin, it has limits derived by being a build-time tool: it will only work for images statically referenced in your code.
If you are dynamically changing your <img>
src
attribute, this plugin cannot help you. If you are doing so with a JS framework via dynamic bindings (Vue :src="..."
, Angular [src]="..."
, etc), changing your component to use slots instead could help you and make your components more flexible.
The compilation time overhead of this plugin is pretty high, due to image processing. It is not advisable to use it during development unless you have a really valid motivation to do so. You'll probably want to apply it conditionally to your webpack chain only when building for production.
if (process.env.NODE_ENV === "production") {
webpackConfig.module.rules.push({ ... });
}
When executed into Node environment (eg. when building for Quasar SSR mode) and using responsive-bg
feature, the compilation could break and throw one of these two errors:
-
Conflict: Multiple chunks emit assets to the same filename server-bundle.js
Using multiple Webpack
entry
points, as we do to register the handler, and compiling client and server bundles with the same Webpack process, theresponsive-bg-image-handler
will be registered two times, triggering a naming conflict. -
ReferenceError: window is not defined
This error is thrown when the plugin tries to register the handler into the global
window
object, as it isn't available in the Node environment.
The handler is only useful at runtime on the client, the solution to both these problems is to include the handler registration only on the client webpack configuration.
When talking about Quasar SSR mode, this means you should use isClient
SSR flag into the second parameter of extendWebpack
.
extendWebpack(webpackConfig, { isClient }) {
// ... other configurations
if (isClient) {
webpackConfig.entry['responsive-bg-image-handler'] =
ResponsiveImagePlugin.bgHandler;
}
},
When generating multiple builds by instancing more than one plugin instance running in parallel, eg. Quasar official SSR mode or this community SSG mode, you should mark all instances as dry runs except the first one.
When talking about Quasar SSR mode, this means you should use isClient
or isServer
SSR flags from the second parameter of extendWebpack
.
extendWebpack(webpackConfig, { isServer }) {
webpackConf.plugins.push(
new ResponsiveImagePlugin({
dryRun: isServer,
// ... other configurations
}),
);
},
<img>
will be wrapped into a <picture>
when the plugin kicks in.
Use a class to reference the image in your selectors and avoid direct-descendent selector.
Check out class management into the Usage section.
<div class="container">
<img
class="positioning-class"
responsive
responsive-img-class="inner-image-class"
src="something.jpg"
/>
<img class="my-image" responsive src="something.jpg" />
</div>
will become
<div class="container">
<picture class="positioning-class">
<source />
<source />
<!-- ... -->
<img class="inner-image-class" responsive src="something.jpg" />
</picture>
<picture class="my-image">
<source />
<source />
<!-- ... -->
<img class="my-image" responsive src="something.jpg" />
</picture>
</div>
so the selector should take into accout both structures, depending on the context
/* Should access direct child, whoever it is (eg. positioning or spacing) */
/* (preferred) */
.positioning-class {
/* ... */
}
/* Or */
.container > img,
.container > picture {
/* ... */
}
/* Should access original image tag */
/* (preferred) */
.inner-image-class {
/* ... */
}
/* Or */
/* (preferred) */
.container .my-image {
/* ... */
}
/* Or */
.container img {
/* ... */
}
/* Or */
.container > img,
.container > picture > img {
/* ... */
}
Custom transformation images' path currently cannot contain _
or :
characters, check if your does and if it's the case update the file name!
Conversion and resolution-switching are enabled by default.
If you want to disable them globally, set conversion.converter
and/or resolutionSwitching.resizer
to null
into the plugin options.
Currently there is no way to disable them on a per-image basis.
defaultSize
, which is a global configuration option, will be used both for art direction and resolution switching. In the latter case, it is used in particular when:
- a breakpoint is generated after the last art direction source;
- there are no art direction sources at all.
Because of this, you should set defaultSize
to be the one of the image on the biggest screen possible.
Example: if the image occupies 100% of the viewport width on the maximum supported width of my website, default size
will be 1.0
. If it occupies 50%, default size
will be 0.5
.
The plugin won't process the image if responsive
attribute is missing or if src
attribute is missing or empty. Also, art direction won't take place if responsive-ad
is missing.
You must manually prevent the fallback background-image CSS rule from being applied when the plugin kicks in.
Remember to wrap it into a :not([data-responsive-bg])
selector!
Due to poor flexibility of image-set()
CSS function (HTML srcset
attribute counterpart), background images management exploits the same HTML features used for <img>
tags.
An hidden <picture>
element, whose purpose is to detect the best image to use, is added as the first child of the enhanced element. This could break CSS cardinality selectors like :first-child
, :first-of-type
and :nth-child
.
The enhanced element background-image
property is updated via a globally available JavaScript handler every time the <picture>
inner <img>
element loads a new image.
<div class="enhanced-bg-div" responsive responsive-bg="my-little-calogero.jpg">
<p>Hey there, I'm famous</p>
</div>
<!-- WILL BECOME -->
<div
class="enhanced-bg-div"
responsive
responsive-bg="my-little-calogero.jpg"
data-responsive-bg
>
<picture class="responsive-bg-holder">
<source />
<source />
<!-- ... -->
<img
class="responsive-bg-holder"
responsive
src="my-little-calogero.jpg"
style="display: none"
onload="**handler invocation**"
/>
</picture>
<p>Hey there, I'm famous</p>
</div>
Please see CONTRIBUTING for details.
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.