Hướng dẫn triển khai cơ chế tải ảnh thông minh tùy biến trên Vue 3

Vấn đề hiệu năng và giải pháp

Trong các ứng dụng Web hiện đại, việc tải toàn bộ tài nguyên media cùng lúc gây lãng phí băng thông và làm giảm tốc độ hiển thị ban đầu. Để tối ưu hóa, chúng ta cần áp dụng kỹ thuật Lazy Load. Dưới đây là quy trình xây dựng một thành phần xử lý ảnh động với chỉ báo chờ, sử dụng Vue 3 và giao thức quan sát vùng hiển thị.

Bước 1: Thiết kế thành phần chỉ báo trạng thái

Trước tiên, hãy tạo một thành phần phụ trợ để hiển thị hoạt ảnh khi dữ liệu chưa sẵn sàng. Thay vì dùng thư viện bên ngoài, ta có thể dùng CSS thuần để tạo hiệu ứng chuyển động hình khối.

<template>
  <div class="spinner-wrapper">
    <!-- Các khối cấu thành animation -->
    <div class="block-item" v-for="n in 7" :key="n" :style="{ '--d': `${n * -0.2}s` }"></div>
  </div>
</template>

<style scoped lang="scss">
.spinner-wrapper {
  width: 80px;
  height: 80px;
  position: relative;
  transform: rotate(45deg);

  .block-item {
    position: absolute;
    top: 0; left: 0;
    width: 24px; height: 24px;
    background-color: #fff;
    animation: move-box 10s ease-in-out infinite both;
    animation-delay: var(--d);
  }

  @keyframes move-box {
    $positions: (
      0%: (0, 0), 12.5%: (32px, 0), 
      25%: (64px, 0), 37.5%: (64px, 32px),
      50%: (32px, 32px), 62.5%: (32px, 64px),
      75%: (0, 64px), 100%: (0, 0)
    );
    
    @each $k, $v in $positions {
      &##{$k} {
        left: nth($v, 1);
        top: nth($v, 2);
      }
    }
  }
}
</style>

Bước 2: Đóng gói thành phần chứa ảnh

Tạo một component đóng vai trò như một container trung gian. Nó sẽ quản lý hai trạng thái: hiển thị bộ nhớ đệm (loading) và nội dung thật sau khi tải xong.

<template>
  <section class="media-container">
    <transition-group name="fade-effect">
      <div key="loader" class="overlay-loader" v-if="isProcessing">
        <SpinnerIcon />
      </div>
      <img 
        v-if="imageUrl" 
        :src="imageUrl" 
        class="real-image" 
        @load="handleSuccess"
      />
    </transition-group>
  </section>
</template>

<script setup>
import { ref } from 'vue';
import SpinnerIcon from './SpinnerIcon.vue';

const props = defineProps({
  sourceUrl: String
});

const imageUrl = ref('');
const isProcessing = ref(true);

const handleSuccess = () => {
  isProcessing.value = false;
};
</script>

<style scoped lang="scss">
.media-container {
  position: relative;
  overflow: hidden;
  
  .overlay-loader {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 2;
  }

  .real-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0;
    transition: opacity 0.5s ease;
    visibility: hidden;

    &.loaded-state {
      opacity: 1;
      visibility: visible;
    }
  }
}
.fade-effect-enter-active, .fade-effect-leave-active {
  transition: opacity 0.3s;
}
</style>

Bước 3: Xây dựng lệnh tùy chỉnh (Custom Directive)

Thay vì gọi hàm thủ công, ta định nghĩa một directive để gắn vào phần tử DOM. Sử dụng IntersectionObserver giúp theo dõi chính xác thời điểm thành phần nằm trong vùng nhìn thấy của cửa sổ.

// plugins/imageLazyPlugin.js
export const registerLazyImage = (app) => {
  app.directive('img-load', {
    mounted(targetElement, binding) {
      const handler = new IntersectionObserver((entries) => {
        entries.forEach((item) => {
          if (item.isIntersecting) {
            const targetImg = targetElement.querySelector('.image-target');
            
            if (targetImg && !targetImg.src) {
              targetImg.src = binding.value;
              // Xử lý sự kiện load được gán trong template
            }
            handler.disconnect();
          }
        });
      }, { threshold: 0.1 });

      handler.observe(targetElement);
    }
  });
};

Lưu ý rằng logic ở trên kiểm tra threshold để đảm bảo ảnh bắt đầu tải khi chiếm ít nhất 10% khung hình.

Bước 4: Tích hợp vào ứng dụng

Kích hoạt directive đã viết tại điểm khởi tạo ứng dụng Vue root.

import { createApp } from 'vue';
import App from './App.vue';
import { registerLazyImage } from './plugins/imageLazyPlugin';

const myApp = createApp(App);
myApp.use(registerLazyImage);
myApp.mount('#root');

Bước 5: Áp dụng thực tế

Khi đã hoàn tất cấu hình, bạn chỉ cần sử dụng thuộc tính v-img-load trên wrapper component hoặc thẻ img tương ứng.

<!-- Trong Template trang chi tiết -->
<div class="gallery-item" v-img-load="productData.imageUrl">
  <!-- Nội dung con bên trong sẽ tự động xử lý logic -->
  <MediaWrapper :source-url="productData.imageUrl" />
</div>

Cách tiếp cận này tách biệt rõ ràng giữa logic hiển thị và logic tải dữ liệu, giúp mã nguồn dễ bảo trì và tái sử dụng.

Thẻ: Vue3 intersectionobserver custom-directive lazy-loading JavaScript

Đăng vào ngày 14 tháng 8 lúc 16:38