15

I have an app with the Vue CLI. When the app loads, I have a bunch of images that appear with a transition when a user clicks a button. The problem is that when the user clicks a button, the corresponding image only then starts to load, meaning that most of the animation is done until then. This makes the experience quite choppy because the images suddenly pop in during the transition, displacing other elements. I want to prefetch them when the site loads.

This answer suggests using the Image class. However, according to the Vue CLI docs, Vue internally uses its own plugin for that, preload-webpack-plugin, and it apparently can be configured.

I tried to configure it so that it preloads images:

vue.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');

module.exports = {
  configureWebpack: {
    plugins: [
      new HtmlWebpackPlugin(),
      new PreloadWebpackPlugin({
        rel: 'prefetch',
        as (entry) {
          if (/\.css$/.test(entry)) return 'style';
          if (/\.woff$/.test(entry)) return 'font';
          if (/\.png$/.test(entry)) return 'image';
          return 'script';
        }
      })
    ]
  }
}

This only manages to screw up the final index.html, leaving it without the build scripts and styles inside.

If I remove this line:

      new HtmlWebpackPlugin(),

The site still loads but the images are not prefetched. It's the same as if I never did anything in the vue.config.js file.

How do I set it up correctly?


Edit: In Vue components, I use require() to load the images, meaning they pass through Webpack. For example:

<img :src="require('../assets/img/' + img)" draggable="false">

Edit: I was able to prefetch the images as Roy J suggested in the comments:

PreloadImages.vue in my main component:

<template>
  <div style="display: none;">
    <img :src="require('../assets/img/foo.png')">
    <img :src="require('../assets/img/bar.png')">
    <img :src="require('../assets/img/baz.png')">
  </div>
</template>

However, that's not the answer to my actual question - it doesn't use resource hints via <link> tags. It also requires more effort and I believe it's a bad practice.

dodov
  • 5,206
  • 3
  • 34
  • 65
  • The Vue docs you linked to seem to suggest to do [this](https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content#Including_media) instead. –  Jan 14 '19 at 20:44
  • Yes, if I want to hardcode the prefetch meta tags. I have a build system that's supposed to do that, I want to give _it_ the satisfaction. If I put the tags in my `index.html`, the images won't pass through neither Vue or Webpack. Plus, it can't work with dynamic assets because their filenames are hashed. – dodov Jan 14 '19 at 20:49
  • Can you include the images in `` tags somewhere that isn't visible? – Roy J Jan 14 '19 at 21:17
  • 1
    I guess I can, but that's a bad solution I think? First, I need to duplicate the `` tags, which is not DRY. Second, the paths are dynamic, which makes things even more complicated. – dodov Jan 14 '19 at 21:40
  • You would make the img tags dynamic, of course, just like the visible ones. – Roy J Jan 14 '19 at 23:05
  • But the images are rendered inside an SFC and the `img` variable is a prop. I don't have access to the prop outside of the component. Also, I can't put the hidden `` _inside_ the component because, well, it will load the image when the component is mounted, which is pointless. – dodov Jan 14 '19 at 23:14
  • Here's Vue example code that preloads Images using the basic JS method: https://codesandbox.io/s/zn1282zz4 –  Jan 14 '19 at 23:31
  • That's a useful link, but it's not the solution to my problem. I mean, it doesn't use resource hints (and also, it implements resource preload, not prefetch). I ended up just duplicating the images as you suggested (see my edit above). – dodov Jan 15 '19 at 00:00
  • I have very little experience using Vue CLI. But as far as I can see, if you want this to be true "meaning they pass through Webpack." you will need to reference static assets relatively `./` - https://cli.vuejs.org/guide/html-and-static-assets.html#static-assets-handling. – jaredrethman Feb 17 '19 at 00:59

2 Answers2

5

Since the plugin is already included by VueJS I think you have to modify it with chainWebpack.

According to the preload webpack plugin documentation, you should also set include option to 'allAssets' value.

It is very common in Webpack to use loaders such as file-loader to generate assets for specific types, such as fonts or images. If you wish to preload these files as well, you can use include with value allAssets

So the configuration will be something like this:

// vue.config.js
module.exports = {
    chainWebpack: config => {
        config.plugin('preload').tap(options => {
            options[0].as = (entry) => {
                if (/\.css$/.test(entry)) return 'style';
                if (/\.woff$/.test(entry)) return 'font';
                if (/\.png$/.test(entry)) return 'image';
                return 'script';
              }
            options[0].include = 'allAssets'
            // options[0].fileWhitelist: [/\.files/, /\.to/, /\.include/]
            // options[0].fileBlacklist: [/\.files/, /\.to/, /\.exclude/]
            return options
        })
    }
}

With a fresh Vue-cli installation I got the following HTML generated

<!DOCTYPE html>
<html lang=en>

<head>
    <meta charset=utf-8>
    <meta http-equiv=X-UA-Compatible content="IE=edge">
    <meta name=viewport content="width=device-width,initial-scale=1">
    <link rel=icon href=/favicon.ico> <title>vue-preload-images</title>
    <link href=/css/app.0b9b292a.css rel=preload as=style>
    <!-- Here is the logo image -->
    <link href=/img/logo.82b9c7a5.png rel=preload as=image>
    <link href=/js/app.30d3ed79.js rel=preload as=script>
    <link href=/js/chunk-vendors.3d4cd4b5.js rel=preload as=script>
    <link href=/css/app.0b9b292a.css rel=stylesheet>
</head>

<body><noscript><strong>We're sorry but vue-preload-images doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript>
    <div id=app></div>
    <script src=/js/chunk-vendors.3d4cd4b5.js> </script> <script src=/js/app.30d3ed79.js> </script> </body> </html>

I hope it will work for you.

lbineau
  • 73
  • 2
  • 9
1

Solution 1.

When the user clicks the button you render your image in a non-visible state.

On image's load event you perform your transition smoothly.

If the loading of the image typically takes more than 2 seconds, consider giving the user a visual clue the button click was recorded and stuff is about to happen.

This would be the technically correct solution.


Solution 2.

An alternative, used extensively in production environments, (and having nothing to do with Vue per-se), is to load thumbnails of the images in your page (as in 10 times smaller - which is ~100 times smaller in size, being areas). It might not be obvious, but they make very good substitutes for large ones, while the transition is ongoing - you might also want to experiment with CSS blur filter on them.

The thumbnail and the large image have to be perfectly overlapped, in the same transitioning parent, with the large one underneath. On load event of the large version, fade the thumbnail out, causing a focus-in effect: a subtle eye-catcher.
A rather funny perk of this method is that, if you leave the thumbs on top (with opacity: 0), whenever someone tries to download an image they right click the thumb and have trouble understanding why they're downloading it at such a low res.

Everything is pretty much the same (in terms of animations) with the addition of a focus-in effect on the image, when it actually loaded.

Not DRY, but efficient. Looks professional, everything seems to load instantly and smoothly. Overall, it converts way more than other "correct" solutions. Which is why it's commonly used on websites where page performance is important.


When dealing with visuals, being perfect is looking perfect (and, in UX, feeling/seeming perfect).
Ancient greeks bent their columns so they looked straight when watched from below.

tao
  • 82,996
  • 16
  • 114
  • 150