UNPKG

@asciisd/vue-progressive-iframe

Version:

A Vue 3 component for progressive iframe loading with advanced content detection

395 lines (315 loc) 14.2 kB
# Vue Progressive Iframe A Vue 3 component library for progressive iframe loading with advanced content detection. Perfect for embedding third-party content like analytics dashboards, trading platforms, or any iframe-based applications. ## Features - **Multiple Loading Strategies**: Choose from 5 optimized loading strategies based on [Aaron Peters' research](https://www.aaronpeters.nl/blog/iframe-loading-techniques-and-performance/) - **Progressive Loading**: Show iframe immediately with loading overlay (default) - **Dynamic Async**: Meebo's ultra-awesome technique - no onload blocking, no busy indicators - **Smart Content Detection**: Automatically detect when iframe content is actually ready - **Performance Optimized**: Prevent main page onload blocking and minimize browser busy indicators - **Cross-Origin Support**: Works with cross-origin iframes using intelligent heuristics - **TypeScript Support**: Full TypeScript support with proper type definitions - **Customizable UI**: Flexible loading states and error handling - **Vue 3 Composition API**: Built with modern Vue 3 patterns - **Lightweight**: Minimal dependencies, optimized bundle size ## 🚀 Installation [![npm version](https://badge.fury.io/js/@asciisd%2Fvue-progressive-iframe.svg)](https://www.npmjs.com/package/@asciisd/vue-progressive-iframe) [![npm downloads](https://img.shields.io/npm/dm/@asciisd/vue-progressive-iframe.svg)](https://www.npmjs.com/package/@asciisd/vue-progressive-iframe) [![license](https://img.shields.io/npm/l/@asciisd/vue-progressive-iframe.svg)](https://github.com/aemaddin/vue-progressive-iframe/blob/main/LICENSE) ```bash npm install @asciisd/vue-progressive-iframe ``` **Requirements:** - Vue 3.5+ - Node.js 16+ - TypeScript 5+ (if using TypeScript) ### Plugin Installation (Optional) If you want to register the component globally: ```typescript import { createApp } from "vue"; import { VueProgressiveIframe } from "@asciisd/vue-progressive-iframe"; const app = createApp({}); app.use(VueProgressiveIframe); ``` ## 📖 Usage ### Basic Usage ```vue <template> <ProgressiveIframe :src="iframeUrl" :height="600" @content-loaded="onContentLoaded" @load-error="onLoadError" /> </template> <script setup> import { ProgressiveIframe } from "@asciisd/vue-progressive-iframe"; const iframeUrl = "https://example.com/dashboard"; const onContentLoaded = (loadTime) => { console.log(`Content loaded in ${loadTime}ms`); }; const onLoadError = (error) => { console.error("Failed to load iframe content:", error); }; </script> ``` ### Performance-Optimized Usage ```vue <template> <ProgressiveIframe :src="iframeUrl" :height="800" loading-strategy="dynamic-async" :prevent-onload-blocking="true" :minimize-busy-indicators="true" :content-detection-timeout="30000" :show-debug-info="true" @content-loaded="onContentLoaded" @detection-progress="onProgress" > <template #loading> <div class="custom-loading"> <h3>Loading Dashboard...</h3> <progress :value="progress" max="100"></progress> </div> </template> <template #error="{ error, retry }"> <div class="custom-error"> <h3>Failed to Load</h3> <p>{{ error.message }}</p> <button @click="retry">Try Again</button> </div> </template> </ProgressiveIframe> </template> ``` ### Using the Composable ```vue <script setup> import { useProgressiveIframe } from "@asciisd/vue-progressive-iframe"; const { iframeRef, isContentLoading, hasError, errorMessage, contentCheckCount, forceLoad, refresh, } = useProgressiveIframe({ src: "https://example.com/dashboard", contentDetectionTimeout: 30000, maxContentChecks: 30, crossOriginWaitTime: 10000, }); </script> <template> <div class="relative"> <iframe ref="iframeRef" :src="src" class="w-full h-96" :class="{ 'opacity-30': isContentLoading }" /> <div v-if="isContentLoading" class="absolute inset-0 flex items-center justify-center" > <div>Loading... ({{ contentCheckCount }}/30)</div> </div> <div v-if="hasError" class="absolute inset-0 flex items-center justify-center bg-red-50" > <div> <p>{{ errorMessage }}</p> <button @click="forceLoad">Force Load</button> <button @click="refresh">Retry</button> </div> </div> </div> </template> ``` ## 🚀 Loading Strategies Choose the optimal loading strategy based on your use case: | Strategy | Blocks Onload | Busy Indicator | Best For | Performance | | --------------- | ------------- | -------------- | ------------------------------------ | ----------- | | `progressive` | No | No | Default choice, immediate visibility | ⭐⭐⭐⭐⭐ | | `dynamic-async` | No | No | Third-party widgets, ads | ⭐⭐⭐⭐⭐ | | `friendly` | No | No | Same-domain content, widgets | ⭐⭐⭐⭐⭐ | | `after-onload` | No | Yes | Below-fold content | ⭐⭐⭐⭐ | | `settimeout` | No\* | Yes | Legacy compatibility | ⭐⭐⭐ | | `traditional` | Yes | Yes | Critical content only | ⭐⭐ | \*Note: setTimeout strategy may block onload in IE8 ### Strategy Recommendations ```typescript import { getRecommendedStrategy } from "@asciisd/vue-progressive-iframe"; // For third-party analytics dashboard const strategy = getRecommendedStrategy({ isThirdParty: true, isAboveFold: true, isCritical: false, blockingTolerance: "none", }); // Returns: 'dynamic-async' // For critical above-the-fold content const strategy = getRecommendedStrategy({ isThirdParty: false, isAboveFold: true, isCritical: true, blockingTolerance: "medium", }); // Returns: 'progressive' ``` ### Friendly Iframe for Same-Domain Content The **Friendly Iframe** technique is perfect for same-domain content, ads, and widgets. Inspired by [vue-friendly-iframe](https://github.com/officert/vue-friendly-iframe) and IAB recommendations: ```vue <template> <!-- Embed HTML content directly --> <ProgressiveIframe loading-strategy="friendly" :html-content="widgetHtml" :styles="['body { margin: 0; padding: 20px; }']" :auto-resize="true" :enable-bridge="true" @bridge-message="handleMessage" /> </template> <script setup> const widgetHtml = ` <div> <h3>Analytics Widget</h3> <div id="stats">Loading...</div> <button onclick="updateStats()">Refresh</button> <script> // Set flag for friendly iframe window.inDapIF = true; function updateStats() { // Send message to parent parent.postMessage({ type: 'iframe_bridge_stats_updated', payload: { views: 1234, users: 567 } }, '*'); } </script> </div> ` const handleMessage = (data) => { if (data.stats_updated) { console.log('Stats updated:', data.payload) } } </script> ``` #### Friendly Iframe Benefits - **Same-domain access** - Full control over iframe content - **Auto-resize** - Automatically adjusts to content height - **PostMessage bridge** - Seamless parent-iframe communication - **No onload blocking** - Doesn't interfere with main page loading - **Keep-alive support** - Preserve state across route changes ## 📦 Available Exports ```typescript import { // Main component ProgressiveIframe, // Composable useProgressiveIframe, // Plugin for global registration VueProgressiveIframe, // Utilities createFriendlyIframe, generateSandboxAttributes, detectContentReady, // Types type IframeLoadingStrategy, type ProgressiveIframeOptions, type ProgressiveIframeEvents, } from "@asciisd/vue-progressive-iframe"; ``` ## 🔧 API Reference ### ProgressiveIframe Component #### Props | Prop | Type | Default | Description | | ------------------------- | ----------------------- | ------------------------------------------------------------ | ----------------------------------------- | | `src` | `string` | **required** | The iframe source URL | | `height` | `number \| string` | `600` | Iframe height in pixels or CSS value | | `width` | `number \| string` | `'100%'` | Iframe width in pixels or CSS value | | `contentDetectionTimeout` | `number` | `30000` | Timeout for content detection (ms) | | `maxContentChecks` | `number` | `30` | Maximum number of content checks | | `crossOriginWaitTime` | `number` | `10000` | Wait time for cross-origin detection (ms) | | `showDebugInfo` | `boolean` | `false` | Show debug information | | `allowedOrigins` | `string[]` | `[]` | Allowed origins for postMessage | | `sandbox` | `string` | `'allow-scripts allow-same-origin allow-forms allow-popups'` | Iframe sandbox attributes | | `loadingStrategy` | `IframeLoadingStrategy` | `'progressive'` | Loading strategy to use | | `preventOnloadBlocking` | `boolean` | `true` | Prevent blocking main page onload | | `minimizeBusyIndicators` | `boolean` | `true` | Minimize browser busy indicators | | `htmlContent` | `string` | `undefined` | HTML content for friendly iframes | | `styles` | `string[]` | `[]` | CSS styles for friendly iframes | | `autoResize` | `boolean` | `false` | Auto-resize iframe to content height | | `enableKeepAlive` | `boolean` | `false` | Enable iframe state preservation | | `enableBridge` | `boolean` | `false` | Enable PostMessage communication bridge | #### Events | Event | Payload | Description | | -------------------- | ------------------------------------------- | ---------------------------------------- | | `content-loaded` | `{ loadTime: number, checkCount: number }` | Fired when content is detected as loaded | | `load-error` | `{ error: Error, errorCount: number }` | Fired when loading fails | | `detection-progress` | `{ checkCount: number, maxChecks: number }` | Fired during content detection | | `force-loaded` | `{ loadTime: number }` | Fired when force load is used | #### Slots | Slot | Props | Description | | --------- | ------------------------------------------------------------- | ----------------- | | `loading` | `{ checkCount: number, maxChecks: number, loadTime: number }` | Custom loading UI | | `error` | `{ error: Error, retry: Function, forceLoad: Function }` | Custom error UI | ### useProgressiveIframe Composable ```typescript function useProgressiveIframe(options: { src: string; contentDetectionTimeout?: number; maxContentChecks?: number; crossOriginWaitTime?: number; allowedOrigins?: string[]; }): { // Refs iframeRef: Ref<HTMLIFrameElement | undefined>; isContentLoading: Ref<boolean>; hasError: Ref<boolean>; errorMessage: Ref<string>; contentCheckCount: Ref<number>; loadStartTime: Ref<number>; // Methods forceLoad: () => void; refresh: () => void; startContentDetection: () => void; // Computed loadTime: ComputedRef<number>; isTimeout: ComputedRef<boolean>; }; ``` ## 🎯 How It Works 1. **Immediate Visibility**: The iframe is rendered and visible immediately 2. **Progressive Detection**: Content detection starts after iframe initialization 3. **Smart Heuristics**: Uses different detection methods for same-origin vs cross-origin 4. **Fallback Layers**: Multiple fallback mechanisms ensure content eventually loads 5. **User Control**: Force load option available if automatic detection fails ### Content Detection Methods - **Same-Origin**: Direct DOM inspection of iframe content - **Cross-Origin**: Property-based detection with timing heuristics - **PostMessage**: Listens for messages from iframe content - **Timeout Fallback**: Graceful degradation after timeout ## 🔧 Development ```bash # Install dependencies npm install # Start development server npm run dev # Build package npm run build # Run tests npm run test # Type checking npm run typecheck # Lint code npm run lint ``` ## 📄 License MIT License - see LICENSE file for details. ## 🤝 Contributing Contributions are welcome! Please read our contributing guidelines and submit pull requests. ## 🐛 Issues If you encounter any issues, please file them on our [GitHub Issues](https://github.com/aemaddin/vue-progressive-iframe/issues) page.