Vue js Prefetch components
Asked Answered
M

5

7

I recently learnt about lazy loading components and started using it. Now I am trying to prefetch the lazy loaded components as well as vue-router routes. But using the chrome devtools I found that lazy loaded chunks are only loaded when we actually navigate to the lazy loaded route (in case of a vue-router route) or when the v-if evaluates to true and the component is rendered (in case of a lazy loaded component).

I have also tried using the webpackPrefetch: true magic string in the router as well as component import statement but doing that does not seem to make any difference.

Project structure:
Master-Detail layout

router config:

import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);

var routes = [  
  {
    path: "/DetailPage",
    component: () => import(/* webpackChunkName: "Detail-chunk" */ "AppModules/views/MyModuleName/DetailPage.vue")
  },
  {
    path: "/MasterPage",
    component: () => import("AppModules/views/MyModuleName/MasterPage.vue")
  }
]

export const router = new Router({
  routes: routes,
  stringifyQuery(query) {
    // encrypt query string here
  }
});

export default router;

Master view:

<template> 
  <div @click="navigate">
    Some text
  </div>
</template>

<script>
export default {
  name: "MasterPage",
  methods: {
    navigate() {
      this.$router.push({
        path: "/DetailPage",
        query: {},
      });
    },
  },
};
</script>

Details page:

<template>
  <div>    
    <my-component v-if="showComponent" />
    <div @click="showComponent = true">Show Component</div>
  </div>
</template>

<script>
const MyComponent = () => import(/* webpackChunkName: "MyComponent-chunk" */ "AppCore/components/AppElements/Helpers/MyComponent");
export default {
  name: "DetailPage",
  components: {
    MyComponent,
  },
  data() {
    return {
      showComponent: false
    }
  }
};
</script>

vue.js.config file:

const path = require("path");

const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;

module.exports = {
  publicPath: "some-url",
  outputDir: "./some/path",
  chainWebpack: webapckConfig => {
    webapckConfig.plugin("html").tap(() => {
      return [
        {
          inject: true,
          filename: "index.html",
          template: "./public/index.html"
        }
      ];
    });
  },
  productionSourceMap: true,
  configureWebpack: {
    plugins: [
      new BundleAnalyzerPlugin({
        analyzerMode: "server",
        generateStatsFile: false,
        statsOptions: {
          excludeModules: "node_modules"
        }
      })
    ],
    output: {
      filename: "some file name",
      libraryTarget: "window"
    },
    module: {
      rules: [
        {
          test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
          use: [
            {
              loader: "url-loader",
              options: {
                limit: 50000,
                fallback: "file-loader",
                outputPath: "/assets/fonts",
                name: "[name].[ext]?hash=[hash]"
              }
            }
          ]
        }
      ]
    },
    resolve: {
      alias: {
        vue$: process.env.NODE_ENV == 'production' ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
        AppCore: path.resolve(__dirname, "..", "..", "AppCoreLite"),
        AppModules: path.resolve(__dirname, "..", "..", "AppModulesLite")
      }
    }
  }
};

Both the async route and component do get split into separate chunks but these chunks are not prefetched.
When I navigate to the master view, I dont see Detail-chunk.[hash].js in the network tab. It gets requested only when the navigate method in the master page is executed (this the correct lazy load behaviour without prefetch).
Now when I am on the details page, MyComponent-chunk.[hash].js is only requested when the showComponent becomes true (on click of a button)
I've also read at a few places that vue-cli v3 does has prefetch functionality enabled by default and webpack magic string is not needed. I also tried that by removing the webpackPrefetch comment but it made no difference.

I did vue-cli-service inspect and found that prefetch plugin is indeed present in the webpack config:

 /* config.plugin('preload') */
    new PreloadPlugin(
      {
        rel: 'preload',
        include: 'initial',
        fileBlacklist: [
          /\.map$/,
          /hot-update\.js$/
        ]
      }
    ),
    /* config.plugin('prefetch') */
    new PreloadPlugin(
      {
        rel: 'prefetch',
        include: 'asyncChunks'
      }
    ),

UPDATE: I tried removing the prefetch webpack plugin using config.plugins.delete('prefetch'); and then using the webpack magic comment: /* webpackPrefetch: true */ but it made no difference.

How do I implement prefetch functionality?

Marathon answered 21/8, 2020 at 8:27 Comment(5)
Is there any way you could recreate it inside code sandbox?Chandos
@Chandos I am not sure if I can do that. I was trying but could not get code-splitting to work in codesandbox. And I dont think this can be demonstrated without code splitting.Marathon
Have you tried to reproduce this on a fresh vue-cli project? I tried it with your examples and prefetch seems to be working correctly on the latest vue-cli. network tab with prefetch requestsProlix
@RicardoTavares can you please share your vue.config.js, package.json and main.js?Marathon
sure, here's a link to a repro github.com/rnunot/so-prefetch-demoProlix
T
1

I solved this by creating a simple prefetch component that loads after a custom amount of time.

Prefetch.vue

<script>
import LazyComp1 from "./LazyComp1.vue";
import LazyComp2 from "./LazyComp2.vue";
export default {
    components:{
        LazyComp1,
        LazyComp2,
    }
}
</script>

App.vue

<template>
    <Prefech v-if="loadPrefetch"></Prefech>
</template>
<script>
export default {
    components: {
        Prefech: () => import("./Prefetch");
    },
    data() {
        return {
            loadPrefetch: false
        }
    },
    mounted() {
        setTimeout(() => {
            this.loadPrefetch = true;
        }, 1000);
    }
}
</script>
Thromboplastin answered 24/3, 2021 at 23:42 Comment(1)
A Prefetch component is for me the best & easiest solution, it allows to prefetch multiple components at the same place. I'll publish a slightly different version below, but the plinciple is the sameLowbrow
P
1

since vue-router-prefetch didn't work for me i ended up doing it manually.

Vue 3 Example - all routes are iterated on page load and async components are loaded

const router = createRouter({
    history: createWebHistory(),
    routes: [{
        path: '/',
        component: HomeView
    }, {
        path: '/about',
        component: () => import('./views/AboutView.vue')
    }]
});

async function preloadAsyncRoutes() {
    // iterate all routes and if the component is async - prefetch it!
    for (const route of router.getRoutes()) {
        if (!route.components) continue;

        // most routes have just a "default" component unless named views are used - iterate all entries just in case
        for (const componentOrImporter of Object.values(route.components)) {
            if (typeof componentOrImporter === 'function') {
                try {
                    // prefetch the component and wait until it finishes before moving to the next one
                    await componentOrImporter();
                } catch (err) {
                    // ignore failing requests
                }
            }
        }
    }
}

window.addEventListener('load', preloadAsyncRoutes);
Pascia answered 15/2, 2022 at 15:44 Comment(0)
L
0

Lazy loaded components are meant to be loaded only when user clicks the route. If you want to load component before it, just don't use lazy loading.

vue-router will load components to memory and swap the content of the tag dynamically even if you will use normally loaded component.

Lesterlesya answered 23/8, 2020 at 18:31 Comment(3)
I know that lazy loaded loaded components are meant to be loaded only when user clicks the route. But my question was how to implement prefetch functionality along with lazy loading so that it does load components before actually the user demands for it.Marathon
I'm with @ravikumar on this. What we want is this: Initially, the app should only load what is needed to display the start page, and then the browser can prefetch the most common pages while idle. Currently, we can either load everything at the start or lazy load if needed. In both cases, we have longer loading times for the requested page.Bridge
Thank you guys, Now I understands what was the purpose of the question :)Lesterlesya
I
0

You need to implement vue-router-prefetch package for your need. Here is a working demo.

Note: From the working demo, you can notice from console.log that only page 2 is prefetched by the QuickLink component imported from vue-router-prefetch

Code :

import Vue from "vue";
import Router from "vue-router";
import RoutePrefetch from "vue-router-prefetch";

Vue.use(Router);

Vue.use(RoutePrefetch, {
  componentName: "QuickLink"
});

const SiteNav = {
  template: `<div>
    <ul>
      <li>
        <router-link to="/page/1">page 1</router-link>
      </li>
      <li>
        <quick-link to="/page/2">page 2</quick-link>
      </li>
      <li>
        <router-link to="/page/3">page 3</router-link>
      </li>
    </ul>
  </div>`
};
const createPage = (id) => async() => {
  console.log(`fetching page ${id}`);
  return {
    template: `<div>
          <h1>page {id}</h1>
          <SiteNav />
        </div>`,
    components: {
      SiteNav
    }
  };
};

const routers = new Router({
  mode: "history",
  routes: [{
    path: "/",
    component: {
      template: `<div>
          <h1>hi</h1>
          <SiteNav />
        </div>`,
      components: {
        SiteNav
      }
    }
  }]
});

for (let i = 1; i <= 3; i++) {
  routers.addRoutes([{
    path: `/page/${i + 1}`,
    component: createPage(i + 1)
  }]);
}

export default routers;
Inconclusive answered 24/8, 2020 at 4:20 Comment(1)
I think this only works with <router-link>. But as shown in the code in the question, I am programmatically pushing a route to this.$router. Also, this package is only meant for vue-router whereas I am lookingg for a generic solution that works with custom components also (.vue files) like mentioned here: cli.vuejs.org/guide/html-and-static-assets.html#prefetchMarathon
L
0

I'm working on a mobile app. and wanted to load some components dynamically while showing the splash screen.

@Thomas's answer is a good solution (a Prefetch component), but it doesn't load the component in the shadow dom, and Doesn't pass Vetur validation (each component must have its template)

Here's my code:

main.vue

<template>
    <loader />
</template>
<script>
    import Loader from './Loader'
    const Prefetch = () => import('./Prefetch')
    export default {
        name: 'Main',
        components: {
            Loader,
            Prefetch
        }
    }
</script>

Prevetch.vue

<template>
    <div id="prefetch">
        <lazy-comp-a />
        <lazy-comp-b />
    </div>
</template>
<script>
import Vue from 'vue'

import LazyCompA from './LazyCompA'
import LazyCompB from './LazyCompB'

Vue.use(LazyCompA)
Vue.use(LazyCompB)
export default {
    components: {
        LazyCompA,
        LazyCompB
    }
}
</script>

<style lang="scss" scoped>
#prefetch {
    display: none !important;
}
</style>

The loader component is loaded & rendered, then the Prefetch component can load anything dynamically.

Lowbrow answered 1/7, 2021 at 18:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.