MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue懒加载 最佳实践与代码优化策略

2021-11-256.1k 阅读

Vue 懒加载基础概念

在前端开发中,懒加载(Lazy Loading)是一种优化页面性能的重要技术。它的核心思想是延迟加载那些在页面初始渲染时不需要立即呈现的资源,直到真正需要时再进行加载。这对于提高页面的初始加载速度、节省用户流量以及提升用户体验都有着显著的效果。

在 Vue 项目里,懒加载通常应用于组件和图片等资源。以组件懒加载为例,当一个页面包含大量组件,但某些组件在页面初次加载时用户不会马上看到,就可以采用懒加载策略,只有当这些组件进入视口或者满足特定条件时才进行加载。

组件懒加载原理

Vue 实现组件懒加载主要依赖于 ES2015 的动态 import() 语法。在传统的 Vue 组件引入方式中,我们使用 import 语句静态导入组件,例如:

import MyComponent from './components/MyComponent.vue';

这样在打包时,MyComponent 会被直接打包进主 bundle 文件中。而懒加载则通过动态 import() 实现:

const MyComponent = () => import('./components/MyComponent.vue');

这种方式下,MyComponent 组件在运行时才会被加载,而不是在打包阶段就被包含进主 bundle。Webpack 等打包工具会将动态导入的组件分割成单独的 chunk 文件,当需要使用该组件时,Vue 会异步加载这个 chunk 文件。

图片懒加载原理

对于图片懒加载,其原理基于浏览器的 IntersectionObserver API(在不支持该 API 的浏览器中可以使用 scroll 事件模拟)。IntersectionObserver 可以异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉变化的情况。

在 Vue 中实现图片懒加载,我们可以创建一个指令(directive)。当指令绑定到图片元素时,通过 IntersectionObserver 监听图片是否进入视口。如果进入视口,则将图片的真实 src 赋值给 img 标签的 src 属性,从而触发图片加载。

Vue 组件懒加载的最佳实践

路由组件懒加载

在 Vue Router 中使用懒加载可以极大地优化页面加载性能。假设我们有一个简单的 Vue 项目,包含首页、关于页和用户详情页等多个页面。

  1. 传统路由配置 传统方式下,我们会像这样配置路由:
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import About from './views/About.vue';
import User from './views/User.vue';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: About
    },
    {
      path: '/user/:id',
      name: 'user',
      component: User
    }
  ]
});

在这种配置下,HomeAboutUser 组件会在项目启动时就被打包进主 bundle 文件。如果项目较大,包含很多路由组件,主 bundle 文件会变得非常大,导致初始加载时间变长。 2. 懒加载路由配置 使用懒加载后,路由配置如下:

import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('./views/Home.vue')
    },
    {
      path: '/about',
      name: 'about',
      component: () => import('./views/About.vue')
    },
    {
      path: '/user/:id',
      name: 'user',
      component: () => import('./views/User.vue')
    }
  ]
});

这样,只有当用户访问对应的路由时,相关的组件才会被加载。Webpack 会将每个路由组件打包成单独的 chunk 文件,有效减小了主 bundle 文件的大小。

列表组件懒加载

在开发列表页面时,当列表数据量很大,且包含很多复杂的子组件时,懒加载可以显著提升性能。例如,我们有一个商品列表,每个商品项是一个包含图片、描述、价格等信息的组件。

  1. 创建列表组件 首先,创建商品列表组件 ProductList.vue
<template>
  <div>
    <ul>
      <li v-for="product in products" :key="product.id">
        <Product :product="product" />
      </li>
    </ul>
  </div>
</template>

<script>
// 传统导入
// import Product from './Product.vue';

export default {
  data() {
    return {
      products: []
    };
  },
  mounted() {
    // 模拟从后端获取数据
    this.products = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Product ${i}` }));
  }
};
</script>

在上述代码中,如果 Product 组件比较复杂,且在页面初始加载时所有商品项都被渲染,会导致性能问题。 2. 懒加载商品组件 我们使用懒加载来优化 Product 组件的加载:

<template>
  <div>
    <ul>
      <li v-for="product in products" :key="product.id">
        <component :is="ProductComponent" :product="product" />
      </li>
    </ul>
  </div>
</template>

<script>
const ProductComponent = () => import('./Product.vue');

export default {
  data() {
    return {
      products: [],
      ProductComponent
    };
  },
  mounted() {
    // 模拟从后端获取数据
    this.products = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Product ${i}` }));
  }
};
</script>

通过这种方式,只有当每个商品项需要被渲染时,对应的 Product 组件才会被加载,而不是在页面初始加载时就全部加载。

图片懒加载的最佳实践

基于 IntersectionObserver 的图片懒加载指令

  1. 创建指令 在 Vue 中,我们可以创建一个自定义指令来实现图片懒加载。在项目的 directives 目录下创建 lazyLoad.js 文件:
export default {
  inserted(el, binding) {
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value;
          observer.unobserve(el);
        }
      });
    });
    observer.observe(el);
  }
};

然后在 main.js 中注册该指令:

import Vue from 'vue';
import lazyLoad from './directives/lazyLoad';

Vue.directive('lazy-load', lazyLoad);
  1. 在模板中使用指令 在组件模板中,我们可以这样使用 lazy - load 指令:
<template>
  <div>
    <img v - lazy - load="imageUrl" alt="Lazy Loaded Image" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      imageUrl: 'https://example.com/image.jpg'
    };
  }
};
</script>

当图片进入视口时,IntersectionObserver 会触发回调,将真实的图片 src 赋值给 img 标签,从而实现图片的懒加载。

处理加载失败和占位图

  1. 加载失败处理 为了提高用户体验,我们需要处理图片加载失败的情况。在 lazyLoad.js 指令中添加加载失败处理逻辑:
export default {
  inserted(el, binding) {
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value;
          el.onerror = () => {
            el.src = 'https://example.com/fallback.jpg';
          };
          observer.unobserve(el);
        }
      });
    });
    observer.observe(el);
  }
};

这样当图片加载失败时,会显示一个备用图片。 2. 占位图 添加占位图可以让用户在图片加载前有更好的视觉预期。在模板中添加占位图:

<template>
  <div>
    <img v - lazy - load="imageUrl" alt="Lazy Loaded Image" :src="placeholderUrl" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      imageUrl: 'https://example.com/image.jpg',
      placeholderUrl: 'https://example.com/placeholder.jpg'
    };
  }
};
</script>

当真实图片加载完成后,会替换掉占位图。

Vue 懒加载的代码优化策略

优化懒加载组件的预加载

虽然懒加载能有效延迟组件加载,但我们也可以通过预加载来进一步提升用户体验。例如,在路由组件懒加载场景下,当用户在浏览页面时,我们可以提前加载一些可能会用到的路由组件。

  1. 使用 router.beforeEach 进行预加载router.js 中,我们可以利用 router.beforeEach 钩子函数实现预加载:
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');
const User = () => import('./views/User.vue');

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: About
    },
    {
      path: '/user/:id',
      name: 'user',
      component: User
    }
  ]
});

router.beforeEach((to, from, next) => {
  const preloadComponents = [];
  if (to.path === '/about') {
    preloadComponents.push(About);
  } else if (to.path === '/user') {
    preloadComponents.push(User);
  }
  Promise.all(preloadComponents.map(component => component())).then(() => {
    next();
  });
});

export default router;

在上述代码中,当路由即将发生变化时,我们根据目标路由提前加载可能会用到的组件。这样当用户真正访问到目标页面时,组件已经加载完成,能够更快地呈现给用户。

优化图片懒加载的性能

  1. 设置合适的阈值 IntersectionObserver 提供了 threshold 参数,我们可以通过设置合适的阈值来优化图片懒加载性能。threshold 表示目标元素与视口交叉区域的比例,默认值为 0,即目标元素一进入视口就触发回调。如果我们设置 threshold0.5,则表示目标元素有 50% 的区域进入视口时触发回调。 在 lazyLoad.js 指令中设置阈值:
export default {
  inserted(el, binding) {
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value;
          observer.unobserve(el);
        }
      });
    }, {
      threshold: 0.5
    });
    observer.observe(el);
  }
};

这样可以提前加载图片,避免用户滚动时图片加载不及时的情况。 2. 批量加载 对于大量图片的懒加载场景,我们可以采用批量加载的方式来减少 IntersectionObserver 的实例数量,从而提高性能。我们可以将图片分组,例如每 10 张图片为一组,为每组创建一个 IntersectionObserver 实例。

export default {
  inserted(el, binding) {
    const group = Math.floor(binding.arg / 10);
    if (!this.groupObservers[group]) {
      this.groupObservers[group] = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            entry.target.src = entry.target.dataset.src;
            observer.unobserve(entry.target);
          }
        });
      });
    }
    this.groupObservers[group].observe(el);
  },
  data() {
    return {
      groupObservers: {}
    };
  }
};

在模板中使用时,为每个图片添加 data - src 存储真实 src,并通过指令参数标识图片所属组:

<template>
  <div>
    <img v - lazy - load:0="imageUrl1" data - src="https://example.com/image1.jpg" alt="Image 1" />
    <img v - lazy - load:0="imageUrl2" data - src="https://example.com/image2.jpg" alt="Image 2" />
    <!-- 更多图片 -->
  </div>
</template>

通过这种方式,可以有效减少 IntersectionObserver 的实例数量,提高图片懒加载的性能。

懒加载与代码分割的协同优化

  1. 合理配置 Webpack 代码分割 在 Vue 项目中,Webpack 是常用的打包工具。我们可以通过合理配置 Webpack 的代码分割来进一步优化懒加载。默认情况下,Webpack 会根据动态 import() 进行代码分割,但我们可以通过 splitChunks 配置来更精细地控制。 在 webpack.config.js 中添加如下配置:
module.exports = {
  // 其他配置项
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name:'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

上述配置中,splitChunks 配置了如何对代码进行分割。chunks: 'all' 表示对所有类型的 chunk 进行分割;minSize 设置了分割的最小文件大小;cacheGroups 定义了缓存组,这里将来自 node_modules 的模块分割到 vendors 文件中。通过这样的配置,可以使懒加载的组件 chunk 文件更加合理,进一步提升加载性能。 2. 动态导入的命名规则优化 在使用动态 import() 进行组件懒加载时,我们可以给异步导入的模块命名,这样在 Webpack 打包时生成的 chunk 文件会有更清晰的命名。例如:

const MyComponent = () => import(/* webpackChunkName: "my - component - chunk" */ './components/MyComponent.vue');

通过 /* webpackChunkName: "my - component - chunk" */ 这样的注释,Webpack 会将该组件打包成名为 my - component - chunk.js 的文件。这不仅便于在开发和调试过程中识别,也有助于代码的维护和性能优化。

懒加载在大型项目中的应用与优化

大型项目中的组件懒加载策略

  1. 多级路由与嵌套组件的懒加载 在大型 Vue 项目中,路由结构往往比较复杂,存在多级路由和嵌套组件。例如,一个电商项目可能有商品分类路由,每个分类下又有商品列表和商品详情等嵌套组件。 假设我们有如下路由结构:
const router = new Router({
  routes: [
    {
      path: '/category/:categoryId',
      component: () => import('./views/Category.vue'),
      children: [
        {
          path: 'list',
          component: () => import('./views/ProductList.vue'),
          children: [
            {
              path: ':productId',
              component: () => import('./views/ProductDetail.vue')
            }
          ]
        }
      ]
    }
  ]
});

对于这种情况,我们需要确保各级路由组件和嵌套组件都能合理地进行懒加载。在 Category.vue 模板中,嵌套的 ProductList 组件可以这样懒加载:

<template>
  <div>
    <router - view />
    <component :is="ProductListComponent" v - if="$route.path.includes('list')" />
  </div>
</template>

<script>
const ProductListComponent = () => import('./views/ProductList.vue');

export default {
  components: {
    ProductListComponent
  }
};
</script>

ProductList.vue 中,对于 ProductDetail 组件的懒加载同理。通过这种方式,在大型项目中可以有效地控制组件的加载时机,避免一次性加载过多组件导致性能问题。 2. 动态组件加载与懒加载结合 在一些大型项目中,可能需要根据用户的操作或业务逻辑动态加载不同的组件。例如,一个后台管理系统,用户在不同权限下可能看到不同的菜单和功能组件。 假设我们有一个 DynamicComponentLoader.vue 组件,根据用户角色动态加载不同组件:

<template>
  <div>
    <component :is="getComponent" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      userRole: 'admin' // 假设从后端获取用户角色
    };
  },
  computed: {
    getComponent() {
      if (this.userRole === 'admin') {
        return () => import('./components/AdminDashboard.vue');
      } else if (this.userRole === 'user') {
        return () => import('./components/UserDashboard.vue');
      }
    }
  }
};
</script>

这样,只有当用户登录并确定角色后,对应的组件才会被懒加载,进一步优化了大型项目的性能。

大型项目中的图片懒加载优化

  1. 优化图片加载顺序 在大型项目中,图片数量众多,合理的加载顺序可以提升用户体验。例如,对于一个图文并茂的新闻页面,我们可以先加载首屏可见区域的图片,然后再逐步加载其他图片。 我们可以通过给图片添加优先级标识来实现这一点。在 lazyLoad.js 指令中添加对优先级的处理:
export default {
  inserted(el, binding) {
    const priority = binding.arg;
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value;
          observer.unobserve(el);
        }
      });
    }, {
      root: null,
      rootMargin: '0px',
      threshold: priority === 'high'? 0.8 : 0.5
    });
    observer.observe(el);
  }
};

在模板中使用时,给首屏图片添加 high 优先级:

<template>
  <div>
    <img v - lazy - load:high="imageUrl1" data - src="https://example.com/image1.jpg" alt="High Priority Image" />
    <img v - lazy - load="imageUrl2" data - src="https://example.com/image2.jpg" alt="Normal Priority Image" />
    <!-- 更多图片 -->
  </div>
</template>

通过设置不同的阈值,首屏高优先级图片会在更大比例进入视口时提前加载,保证用户在浏览页面时能尽快看到重要图片。 2. 图片资源管理与懒加载的协同 在大型项目中,图片资源的管理至关重要。我们可以采用图片 CDN(内容分发网络)来加速图片加载。同时,结合懒加载技术,能进一步提升性能。 例如,在项目的配置文件中设置图片 CDN 地址:

// config.js
export const IMAGE_CDN = 'https://cdn.example.com';

在图片懒加载指令或模板中使用 CDN 地址:

<template>
  <div>
    <img v - lazy - load="`${IMAGE_CDN}/image.jpg`" alt="Lazy Loaded Image" />
  </div>
</template>

这样,当图片懒加载时,从 CDN 获取图片资源,利用 CDN 的分布式节点和缓存机制,加快图片的加载速度,提升大型项目的整体性能。

应对懒加载可能出现的问题及解决方案

懒加载组件闪烁问题

  1. 问题描述 在某些情况下,当懒加载组件加载完成并插入到页面中时,可能会出现短暂的闪烁现象。这通常是由于组件在加载过程中,占位元素的样式与组件实际渲染后的样式不一致导致的。 例如,在列表组件懒加载场景下,列表项在加载前可能有一个简单的占位文本,当组件加载完成后,新的样式和布局突然出现,就会造成闪烁感。
  2. 解决方案 一种解决方法是在组件加载前,设置好占位元素的样式,使其尽量接近组件渲染后的样式。例如,对于图片懒加载,我们可以根据图片的宽高比例设置占位图的宽高,避免图片加载完成后出现布局跳动。 在组件懒加载场景下,我们可以在模板中设置一个与组件大小和基本样式相似的占位元素:
<template>
  <div>
    <div v - if="!ProductComponent" class="product - placeholder"></div>
    <component :is="ProductComponent" v - if="ProductComponent" :product="product" />
  </div>
</template>

<script>
const ProductComponent = () => import('./Product.vue');

export default {
  data() {
    return {
      ProductComponent: null,
      product: {}
    };
  },
  mounted() {
    ProductComponent().then(component => {
      this.ProductComponent = component.default;
    });
  }
};
</script>

product - placeholder 类中设置与 Product 组件相似的宽度、高度和基本样式,这样在组件加载过程中,页面布局不会发生明显变化,从而避免闪烁问题。

懒加载与 SEO 的冲突及解决

  1. 问题描述 搜索引擎爬虫在抓取页面时,通常不会执行 JavaScript 代码。因此,如果页面内容主要依赖懒加载,爬虫可能无法获取到全部内容,从而影响页面的 SEO 效果。例如,一个博客页面使用懒加载加载文章内容,搜索引擎爬虫可能只能看到标题,而无法获取文章正文。
  2. 解决方案 一种解决方案是采用服务器端渲染(SSR)。在 SSR 模式下,页面在服务器端生成完整的 HTML 内容,包括懒加载组件的内容。搜索引擎爬虫可以直接获取到完整的页面信息。 在 Vue 项目中,可以使用 Nuxt.js 来实现 SSR。Nuxt.js 是一个基于 Vue.js 的服务端渲染框架,它可以自动处理路由、组件懒加载等功能,并生成适合 SEO 的 HTML 页面。 另一种方法是使用静态站点生成(SSG),如 VuePress。在构建时,将页面内容静态化生成 HTML 文件,这样搜索引擎爬虫也能获取到完整的内容。同时,在页面加载后,通过 JavaScript 重新启用懒加载功能,以提升用户体验。

懒加载在低版本浏览器中的兼容性问题

  1. 问题描述 部分低版本浏览器可能不支持 IntersectionObserver API 或动态 import() 语法,这会导致图片懒加载或组件懒加载功能无法正常工作。
  2. 解决方案 对于 IntersectionObserver API 的兼容性问题,可以使用 intersection-observer 垫片库。在项目中安装该库:
npm install intersection-observer

然后在项目入口文件(如 main.js)中引入:

import 'intersection-observer';

这样在不支持 IntersectionObserver 的浏览器中,该垫片库会模拟其功能。 对于动态 import() 语法的兼容性问题,Webpack 会自动处理,通过 Babel 转译将其转换为低版本浏览器支持的语法。但需要确保项目中正确配置了 Babel,在 babel.config.js 中添加如下配置:

module.exports = function (api) {
  api.cache(true);
  const presets = [
    '@vue/cli-plugin - babel/preset',
    [
      '@babel/preset - env',
      {
        targets: {
          browsers: ['ie >= 11']
        },
        useBuiltIns: 'entry',
        corejs: 3
      }
    ]
  ];
  const plugins = [];
  return { presets, plugins };
};

通过上述配置,项目可以在低版本浏览器中正常使用懒加载功能。