ravi kumar
ravi kumar

Reputation: 1620

Vue js Prefetch components

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?

Upvotes: 7

Views: 10688

Answers (5)

MrBar
MrBar

Reputation: 1106

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);

Upvotes: 1

migli
migli

Reputation: 3297

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.

Upvotes: 0

Thomas
Thomas

Reputation: 11

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>

Upvotes: 1

tuhin47
tuhin47

Reputation: 6058

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;

Upvotes: 0

Konrad
Konrad

Reputation: 24661

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.

Upvotes: 0

Related Questions