当前位置: 首页 > news >正文

JavaScript性能优化实战(8):缓存策略与离线优化

前言

在Web应用中,性能优化不仅仅是关于代码执行速度,还与资源获取和数据持久化密切相关。合理的缓存策略可以显著减少网络请求,提升应用响应速度,同时有效降低服务器负载和用户流量消耗。离线优化则进一步解决了网络不稳定或断网场景下的用户体验问题,为Web应用提供类似原生应用的可靠性。

本文作为JavaScript性能优化实战系列的第八篇,将深入探讨前端缓存策略与离线优化技术。我们将从浏览器原生缓存机制出发,逐步深入到Service Worker、IndexedDB等现代Web API,并探讨PWA离线体验优化的实战应用,最终构建离线优先的Web应用架构。

浏览器缓存机制全面解析

浏览器缓存是前端性能优化的第一道防线,合理利用浏览器缓存机制可以大幅减少网络请求,加快页面加载速度。

HTTP缓存基础

HTTP缓存是最基础的缓存形式,通过HTTP头部控制资源的缓存行为。

强缓存

强缓存是指在缓存期间不需要请求服务器,直接使用缓存的资源。主要通过以下HTTP头部控制:

  1. Cache-Control:HTTP/1.1的缓存控制头,优先级高于Expires

    • max-age:缓存有效期(秒)
    • public:可以被任何缓存区缓存
    • private:只能被浏览器缓存
    • no-cache:每次使用缓存前必须先验证资源是否有效
    • no-store:完全不使用缓存
  2. Expires:HTTP/1.0的缓存控制头,指定资源过期的具体时间

// 强缓存示例响应头
Cache-Control: max-age=3600, public
Expires: Wed, 21 Oct 2023 07:28:00 GMT

当浏览器发起请求时,会先检查是否命中强缓存:

  • 如果命中,直接从缓存读取资源,此时Chrome开发者工具的Network面板会显示(from disk cache)(from memory cache)
  • 如果未命中,则进入协商缓存环节
协商缓存

协商缓存是指浏览器需要向服务器发送请求以确认缓存是否有效。主要通过以下HTTP头部控制:

  1. ETag/If-None-Match:资源的唯一标识符

    • 服务器返回资源时设置ETag
    • 浏览器请求时在If-None-Match中带上上次响应的ETag值
  2. Last-Modified/If-Modified-Since:资源的最后修改时间

    • 服务器返回资源时设置Last-Modified
    • 浏览器请求时在If-Modified-Since中带上上次响应的Last-Modified值
// 协商缓存示例
// 服务器响应头
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT// 浏览器后续请求头
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

协商缓存的工作流程:

  • 浏览器发送带有缓存校验字段的请求
  • 服务器判断缓存是否有效
    • 有效:返回304 Not Modified响应,无响应体
    • 无效:返回200 OK响应,带有新的资源
完整的缓存判断流程

下面是一个完整的浏览器缓存判断流程:

发起请求 ---> 是否有缓存? ---> 否 ---> 向服务器请求 --> 返回200,缓存资源|是|强缓存是否有效? ---> 是 ---> 使用缓存,不发送请求 (200 from cache)|否|发送协商缓存请求 ---> 服务器判断缓存是否有效|是|返回304,使用缓存|否|返回200,更新缓存

常见资源类型的缓存策略

不同类型的资源适合不同的缓存策略:

HTML文档

HTML文档通常包含应用的结构和入口,建议使用协商缓存而非强缓存,确保用户能及时获取最新内容:

Cache-Control: no-cache
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
JavaScript和CSS文件

对于带有版本号或哈希的静态资源,可以使用长期的强缓存:

Cache-Control: max-age=31536000, public

在构建系统中可以这样实现:

// webpack配置示例
module.exports = {output: {filename: '[name].[contenthash].js',chunkFilename: '[name].[contenthash].chunk.js'}
};
图片和媒体文件

图片和媒体文件通常变化不频繁,适合使用长期缓存:

Cache-Control: max-age=86400, public
API响应数据

API响应通常是动态数据,不应该被强缓存,但可以根据业务需求使用短期缓存:

Cache-Control: max-age=60, private

对于不应该缓存的敏感数据:

Cache-Control: no-store

缓存控制最佳实践

版本化静态资源

对静态资源使用内容哈希作为文件名的一部分,确保内容变化时URL也随之变化:

// 在构建工具中的配置
{output: {// 使用contenthash确保内容变化时文件名变化filename: '[name].[contenthash:8].js',// 其他资源也使用类似策略assetModuleFilename: 'assets/[name].[contenthash:8][ext]'}
}
合理设置Cache-Control

根据资源类型和更新频率设置适当的Cache-Control:

// Node.js服务器示例
app.use('/static', express.static('public', {etag: true,lastModified: true,setHeaders: (res, path) => {if (path.endsWith('.html')) {// HTML文件使用协商缓存res.setHeader('Cache-Control', 'no-cache');} else if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$/)) {// 带版本号的静态资源使用长期缓存res.setHeader('Cache-Control', 'max-age=31536000, public');}}
}));
使用Vary头处理不同的客户端请求

当响应内容根据请求头变化时,应使用Vary头指定这些头部:

Vary: User-Agent, Accept-Encoding

这确保缓存能正确区分不同客户端或不同压缩格式的响应。

避免缓存私人信息

对于包含用户私人信息的响应,应明确防止缓存:

Cache-Control: private, no-store, max-age=0

实战案例:构建高效的缓存系统

前端缓存层设计

以下是一个典型的前端应用缓存层设计:

[用户请求] --> [Service Worker缓存] --> [HTTP缓存] --> [服务器]|[浏览器存储](IndexedDB/localStorage)
实现多级缓存策略

下面是一个结合HTTP缓存和前端存储的多级缓存实现:

// 封装一个带缓存的API请求函数
async function fetchWithCache(url, options = {}) {const cacheKey = `api:${url}`;const cacheTime = options.cacheTime || 60000; // 默认缓存1分钟// 尝试从localStorage获取缓存try {const cached = localStorage.getItem(cacheKey);if (cached) {const { timestamp, data } = JSON.parse(cached);// 检查缓存是否有效if (Date.now() - timestamp < cacheTime) {console.log('从本地缓存获取数据:', url);return data;}}} catch (e) {console.error('读取缓存出错:', e);}// 发起网络请求,使用HTTP缓存try {const response = await fetch(url, {...options,headers: {...options.headers,// 对于GET请求可以使用协商缓存...(options.method === 'GET' ? {} : { 'Cache-Control': 'no-cache' })}});if (!response.ok) {throw new Error(`请求失败: ${response.status}`);}const data = await response.json();// 将数据存入localStorage缓存try {localStorage.setItem(cacheKey, JSON.stringify({timestamp: Date.now(),data}));} catch (e) {console.error('写入缓存出错:', e);}return data;} catch (error) {console.error('网络请求失败:', error);throw error;}
}// 使用示例
async function getUserProfile(userId) {return fetchWithCache(`/api/users/${userId}`, { cacheTime: 300000 }); // 缓存5分钟
}async function getProductList() {return fetchWithCache('/api/products', { cacheTime: 60000 }); // 缓存1分钟
}

浏览器缓存查看与调试技巧

使用Chrome DevTools检查缓存
  1. 查看HTTP缓存

    • 打开Chrome DevTools > Network面板
    • 勾选"Disable cache"可临时禁用缓存
    • 查看Size列,显示"(from disk cache)“或”(from memory cache)"表示命中缓存
    • 查看Status列,304表示命中协商缓存
  2. 检查应用缓存存储

    • 打开Chrome DevTools > Application面板
    • 在Storage部分可以查看和管理各种缓存和存储
    • Cache Storage查看Service Worker缓存
    • IndexedDB和Local Storage查看对应存储
缓存调试常用命令
// 清除localStorage缓存
localStorage.clear();// 编程方式检查缓存头
fetch('/example.js').then(response => {console.log('Cache-Control:', response.headers.get('Cache-Control'));console.log('ETag:', response.headers.get('ETag'));console.log('Last-Modified:', response.headers.get('Last-Modified'));});// 使用performance API分析缓存性能
performance.getEntriesByType('resource').forEach(resource => {console.log(`${resource.name}: ${resource.duration}ms`);
});

通过深入理解和合理应用浏览器缓存机制,我们可以显著提升Web应用的性能和用户体验。在下一节中,我们将探讨更强大的Service Worker缓存策略,它不仅能实现更细粒度的缓存控制,还能为Web应用提供真正的离线能力。

Service Worker缓存策略与实现

Service Worker是一种运行在浏览器背后的脚本,它独立于网页,能够拦截和处理网络请求,实现离线缓存、推送通知等功能。相比传统的HTTP缓存,Service Worker提供了更精细的缓存控制能力和离线支持。

Service Worker基础

Service Worker生命周期

Service Worker有着明确的生命周期,理解这一点对于实现可靠的缓存策略至关重要:

  1. 注册(Register):告知浏览器Service Worker的位置
  2. 安装(Install):首次安装或更新时触发,通常用于缓存静态资源
  3. 激活(Activate):安装成功后激活,通常用于清理旧缓存
  4. 空闲(Idle):未处理任何事件时的状态
  5. 终止(Terminated):空闲一段时间后被终止,节省资源
  6. 获取(Fetch):拦截网络请求,可以返回缓存或发起新请求
注册Service Worker

首先需要在主页面注册Service Worker:

// 检查浏览器是否支持Service Worker
if ('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/sw.js').then(registration => {console.log('Service Worker注册成功,作用域为:', registration.scope);}).catch(error => {console.error('Service Worker注册失败:', error);});});
}
创建基本的Service Worker

以下是一个基本的Service Worker示例(sw.js):

// 定义缓存名称和缓存资源列表
const CACHE_NAME = 'app-v1';
const CACHE_ASSETS = ['/','/index.html','/css/style.css','/js/main.js','/images/logo.png','/offline.html'
];// 安装事件:预缓存静态资源
self.addEventListener('install', event => {console.log('Service Worker: 安装中');// 延长安装阶段直到缓存完成event.waitUntil(caches.open(CACHE_NAME).then(cache => {console.log('Service Worker: 缓存文件');return cache.addAll(CACHE_ASSETS);}).then(() => self.skipWaiting()) // 强制激活);
});// 激活事件:清理旧缓存
self.addEventListener('activate', event => {console.log('Service Worker: 已激活');event.waitUntil(caches.keys().then(cacheNames => {return Promise.all(cacheNames.map(cache => {if (cache !== CACHE_NAME) {console.log('Service Worker: 清理旧缓存', cache);return caches.delete(cache);}}));}));
});// 拦截fetch请求
self.addEventListener('fetch', event => {console.log('Service Worker: 拦截请求', event.request.url);event.respondWith(// 检查缓存caches.match(event.request).then(response => {// 如果在缓存中找到了响应,返回缓存的版本if (response) {return response;}// 没有缓存,发起网络请求return fetch(event.request).then(response => {// 检查是否得到有效响应if (!response || response.status !== 200 || response.type !== 'basic') {return response;}// 复制响应(因为响应是流,只能消费一次)const responseToCache = response.clone();// 将响应添加到缓存caches.open(CACHE_NAME).then(cache => {cache.put(event.request, responseToCache);});return response;}).catch(() => {// 网络请求失败时,返回离线页面if (event.request.headers.get('accept').includes('text/html')) {return caches.match('/offline.html');}});}));
});

高级缓存策略

Service Worker提供了灵活的缓存策略选择,根据不同资源类型和应用需求,可以实现多种缓存模式:

1. 缓存优先(Cache First)

优先从缓存获取资源,缓存未命中才发起网络请求。适合静态资源和不经常更改的内容。

function cacheFirst(request) {return caches.match(request).then(cacheResponse => {return cacheResponse || fetch(request).then(networkResponse => {// 将网络响应存入缓存return caches.open(CACHE_NAME).then(cache => {cache.put(request, networkResponse.clone());return networkResponse;});});});
}
2. 网络优先(Network First)

优先从网络获取最新资源,网络请求失败才使用缓存。适合经常更新的内容,如API响应。

function networkFirst(request) {return fetch(request).then(networkResponse => {// 将网络响应复制一份存入缓存caches.open(CACHE_NAME).then(cache => {cache.put(request, networkResponse.clone());});return networkResponse;}).catch(() => {// 网络请求失败,使用缓存return caches.match(request);});
}
3. 网络优先并更新缓存(Stale While Revalidate)

同时从缓存和网络获取资源,先返回缓存内容(如果有)以快速响应,同时更新缓存以备下次使用。适合内容更新不那么关键的场景。

function staleWhileRevalidate(request) {return caches.open(CACHE_NAME).then(cache => {return cache.match(request).then(cacheResponse => {const fetchPromise = fetch(request).then(networkResponse => {cache.put(request, networkResponse.clone());return networkResponse;});// 返回缓存响应,或等待网络响应return cacheResponse || fetchPromise;});});
}
4. 缓存与网络竞争(Cache and Network Race)

同时从缓存和网络获取资源,返回最先得到的响应。适合优化在不同网络环境下的体验。

function raceStrategy(request) {// 创建Promise.race,返回最快的响应return Promise.race([caches.match(request).then(response => response),fetch(request).then(response => {// 更新缓存const responseClone = response.clone();caches.open(CACHE_NAME).then(cache => {cache.put(request, responseClone);});return response;})]);
}
5. 缓存回退网络(Cache Falling Back to Network)

先尝试使用缓存,缓存未命中时再发起网络请求。网络请求失败时可以提供通用的离线页面。

function cacheFallbackToNetwork(request) {return caches.match(request).then(cacheResponse => {if (cacheResponse) {return cacheResponse;}return fetch(request).catch(() => {// 请求HTML内容时返回通用离线页面if (request.headers.get('Accept').includes('text/html')) {return caches.match('/offline.html');}// 对于图片可以返回占位图if (request.url.match(/\.(jpg|png|gif|svg)$/)) {return caches.match('/images/offline-image.png');}});});
}

根据资源类型选择缓存策略

在实际应用中,通常需要根据不同的资源类型选择不同的缓存策略:

self.addEventListener('fetch', event => {const request = event.request;const url = new URL(request.url);// 对同源请求应用缓存策略if (url.origin === self.location.origin) {// 静态资源使用缓存优先策略if (url.pathname.startsWith('/static/') || url.pathname.match(/\.(css|js|png|jpg|jpeg|svg|gif)$/)) {event.respondWith(cacheFirst(request));return;}// HTML页面使用网络优先策略if (request.headers.get('Accept').includes('text/html')) {event.respondWith(networkFirst(request));return;}}// API请求使用staleWhileRevalidate策略if (url.pathname.startsWith('/api/')) {event.respondWith(staleWhileRevalidate(request));return;}// 默认使用网络优先策略event.respondWith(networkFirst(request));
});

定期更新缓存内容

有些情况下,我们需要定期更新缓存内容,而不仅依赖于用户的请求:

// 定义需要定期更新的资源
const PERIODIC_UPDATES = ['/api/news','/api/notifications'
];// 设置更新间隔(例如每小时更新一次)
const UPDATE_INTERVAL = 60 * 60 * 1000; // 1小时// 在SW激活后设置定期更新
self.addEventListener('activate', event => {// 其他激活代码...// 设置定期更新任务self.registration.periodicSync.register('update-cache', {minInterval: UPDATE_INTERVAL});
});// 处理定期同步事件
self.addEventListener('periodicsync', event => {if (event.tag === 'update-cache') {event.waitUntil(updateCache());}
});// 更新缓存的函数
function updateCache() {return Promise.all(PERIODIC_UPDATES.map(url => fetch(url).then(response => {if (!response.ok) return;const clonedResponse = response.clone();return caches.open(CACHE_NAME).then(cache => {return cache.put(url, clonedResponse);});}).catch(error => console.error('缓存更新失败:', url, error))));
}

缓存失效与更新策略

管理缓存的更新和失效是Service Worker实现中的关键挑战。以下是几种常用的缓存更新策略:

基于版本号的缓存更新

使用缓存版本号,在Service Worker更新时清除旧缓存:

const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;self.addEventListener('activate', event => {event.waitUntil(caches.keys().then(cacheNames => {return Promise.all(cacheNames.map(cacheName => {// 删除不匹配当前版本的缓存if (cacheName.startsWith('app-cache-') && cacheName !== CACHE_NAME) {return caches.delete(cacheName);}}));}));
});
基于内容哈希的缓存更新

对于静态资源,使用内容哈希作为文件名的一部分,确保内容变化时URL也会变化:

// 在构建过程中生成带哈希的文件名
// main.8e2d4a2.js 而不是 main.js// 在Service Worker中预缓存这些资源
const CACHE_ASSETS = ['/','/index.html','/css/style.5f3e9b1.css','/js/main.8e2d4a2.js','/images/logo.3a7c4d8.png'
];
使用ETag进行缓存验证

对于API响应,可以使用ETag进行缓存验证,只在内容变化时更新缓存:

function updateWithEtag(request) {// 先检查缓存return caches.match(request).then(cachedResponse => {// 提取之前响应的ETagconst etag = cachedResponse ? cachedResponse.headers.get('ETag') : null;// 创建一个新的请求,包含If-None-Match头const newRequest = etag ? new Request(request, {headers: {'If-None-Match': etag}}) : request;// 发送带条件的请求return fetch(newRequest).then(networkResponse => {// 304表示内容未变化,使用缓存if (networkResponse.status === 304) {return cachedResponse;}// 内容已变化,更新缓存并返回新响应const clonedResponse = networkResponse.clone();caches.open(CACHE_NAME).then(cache => {cache.put(request, clonedResponse);});return networkResponse;}).catch(() => {// 网络请求失败,返回缓存(如果有)return cachedResponse;});});
}

处理Service Worker更新

Service Worker更新是一个需要谨慎处理的过程,以下是一种推荐的更新流程:

// 在主页面中监听Service Worker更新
if ('serviceWorker' in navigator) {// 注册Service Workernavigator.serviceWorker.register('/sw.js').then(registration => {// 检查更新registration.addEventListener('updatefound', () => {const newWorker = registration.installing;newWorker.addEventListener('statechange', () => {// 当新的Service Worker变为activated状态if (newWorker.state === 'activated') {// 通知用户页面有更新if (window.confirm('网站已更新,是否刷新页面以应用新版本?')) {window.location.reload();}}});});// 检查控制页面的Service Worker是否发生变化let refreshing = false;navigator.serviceWorker.addEventListener('controllerchange', () => {if (!refreshing) {refreshing = true;window.location.reload();}});});// 定期检查更新setInterval(() => {navigator.serviceWorker.getRegistration().then(registration => {if (registration) {registration.update();}});}, 60 * 60 * 1000); // 每小时检查一次
}

实战案例:构建离线可用的博客应用

以下是一个实际案例,展示如何为博客应用实现离线访问功能:

// sw.js - 博客应用的Service Worker// 缓存名称定义
const STATIC_CACHE = 'blog-static-v1';
const PAGES_CACHE = 'blog-pages-v1';
const IMAGES_CACHE = 'blog-images-v1';
const API_CACHE = 'blog-api-v1';// 需要缓存的静态资源
const STATIC_ASSETS = ['/','/index.html','/css/main.css','/js/app.js','/js/vendor.js','/offline.html','/images/logo.svg','/images/offline.svg'
];// 安装事件 - 预缓存静态资源
self.addEventListener('install', event => {event.waitUntil(caches.open(STATIC_CACHE).then(cache => cache.addAll(STATIC_ASSETS)).then(() => self.skipWaiting()));
});// 激活事件 - 清理旧缓存
self.addEventListener('activate', event => {const currentCaches = [STATIC_CACHE, PAGES_CACHE, IMAGES_CACHE, API_CACHE];event.waitUntil(caches.keys().then(cacheNames => {return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));}).then(cachesToDelete => {return Promise.all(cachesToDelete.map(cacheToDelete => caches.delete(cacheToDelete)));}).then(() => self.clients.claim()));
});// 拦截请求
self.addEventListener('fetch', event => {const request = event.request;const url = new URL(request.url);// 不处理非GET请求if (request.method !== 'GET') {return;}// 处理API请求if (url.pathname.startsWith('/api/')) {event.respondWith(handleApiRequest(request));return;}// 处理HTML页面请求if (request.headers.get('Accept').includes('text/html')) {event.respondWith(handleHtmlRequest(request));return;}// 处理图片请求if (url.pathname.match(/\.(jpg|jpeg|png|gif|svg)$/)) {event.respondWith(handleImageRequest(request));return;}// 处理其他静态资源event.respondWith(caches.match(request).then(cachedResponse => cachedResponse || fetchAndCache(request, STATIC_CACHE)));
});// 处理API请求
function handleApiRequest(request) {return fetchWithTimeout(request, 3000).then(response => {// 复制响应以便缓存const clonedResponse = response.clone();// 只缓存成功的响应if (response.ok) {caches.open(API_CACHE).then(cache => cache.put(request, clonedResponse));}return response;}).catch(() => {// 网络请求失败,尝试从缓存获取return caches.match(request).then(cachedResponse => {if (cachedResponse) {// 给缓存响应添加标记,表明来自缓存const responseOptions = {headers: new Headers(cachedResponse.headers),status: cachedResponse.status,statusText: cachedResponse.statusText};responseOptions.headers.set('X-Data-Source', 'cache');return cachedResponse.blob().then(body => new Response(body, responseOptions));}// 如果没有缓存,返回离线API响应return new Response(JSON.stringify({error: 'offline',message: '您当前处于离线状态,无法获取最新数据'}),{status: 503,headers: { 'Content-Type': 'application/json' }});});});
}// 处理HTML请求
function handleHtmlRequest(request) {return fetchWithTimeout(request, 3000).then(response => {// 复制响应并缓存const clonedResponse = response.clone();caches.open(PAGES_CACHE).then(cache => cache.put(request, clonedResponse));return response;}).catch(() => {// 网络请求失败,尝试从缓存获取return caches.match(request).then(cachedResponse => cachedResponse || caches.match('/offline.html'));});
}// 处理图片请求
function handleImageRequest(request) {// 使用缓存优先策略return caches.match(request).then(cachedResponse => {if (cachedResponse) {// 同时在后台更新缓存fetch(request).then(networkResponse => {if (networkResponse.ok) {caches.open(IMAGES_CACHE).then(cache => cache.put(request, networkResponse));}}).catch(() => {/* 静默失败 */});return cachedResponse;}// 缓存中没有,发起网络请求return fetch(request).then(networkResponse => {// 缓存响应副本const clonedResponse = networkResponse.clone();caches.open(IMAGES_CACHE).then(cache => cache.put(request, clonedResponse));return networkResponse;}).catch(() => {// 返回占位图片return caches.match('/images/offline.svg');});});
}// 通用的获取并缓存函数
function fetchAndCache(request, cacheName) {return fetch(request).then(response => {if (response.ok) {const clonedResponse = response.clone();caches.open(cacheName).then(cache => cache.put(request, clonedResponse));}return response;}).catch(error => {console.error('获取资源失败:', error);throw error;});
}// 添加超时的fetch函数
function fetchWithTimeout(request, timeout) {return new Promise((resolve, reject) => {// 设置超时const timeoutId = setTimeout(() => {reject(new Error('请求超时'));}, timeout);fetch(request).then(response => {clearTimeout(timeoutId);resolve(response);},err => {clearTimeout(timeoutId);reject(err);});});
}

通过上述Service Worker实现,博客应用能够实现:

  1. 离线浏览已访问过的页面
  2. 即使在线,也能快速加载缓存资源
  3. 针对不同资源类型应用不同的缓存策略
  4. 提供优雅的离线体验和失败处理

Service Worker强大的缓存控制能力和离线支持,使它成为现代Web应用性能优化和用户体验提升的关键技术。在下一节中,我们将探讨另一种客户端存储技术——IndexedDB,它为Web应用提供了更强大的本地数据库能力。

IndexedDB高性能客户端存储应用

IndexedDB基础概念

IndexedDB是一种低级API,用于在客户端存储大量结构化数据。与localStorage和sessionStorage不同,IndexedDB提供了完整的事务性数据库系统,支持索引、游标和事务,能够高效处理大量数据。

IndexedDB的主要特点
  1. 大容量存储:可以存储远超localStorage的数据量(通常为数百MB甚至GB)
  2. 结构化数据:支持JavaScript对象直接存储,无需序列化
  3. 事务支持:提供类似传统数据库的事务隔离
  4. 异步API:所有操作都是异步的,不会阻塞主线程
  5. 同源策略:遵循浏览器同源策略,保证安全性
  6. 索引查询:可以为数据创建索引,支持高效查询
IndexedDB的基本工作流程
  1. 打开数据库连接
  2. 在连接回调中创建对象仓库(object store)
  3. 启动一个事务并请求执行操作
  4. 通过事件监听操作结果
  5. 使用查询方法或游标获取数据

创建和管理IndexedDB数据库

打开数据库
function openDatabase() {return new Promise((resolve, reject) => {// 打开数据库(如果不存在则创建)const request = indexedDB.open('MyAppDB', 1);// 处理数据库升级事件request.onupgradeneeded = event => {const db = event.target.result;// 创建对象仓库if (!db.objectStoreNames.contains('users')) {const usersStore = db.createObjectStore('users', { keyPath: 'id' });// 创建索引usersStore.createIndex('email', 'email', { unique: true });usersStore.createIndex('name', 'name', { unique: false });}if (!db.objectStoreNames.contains('articles')) {const articlesStore = db.createObjectStore('articles', { keyPath: 'id' });articlesStore.createIndex('author', 'authorId', { unique: false });articlesStore.createIndex('date', 'publishDate', { unique: false });}};// 成功回调request.onsuccess = event => {const db = event.target.result;resolve(db);};// 错误回调request.onerror = event => {console.error('打开数据库失败:', event.target.error);reject(event.target.error);};});
}
添加数据
function addItem(db, storeName, item) {return new Promise((resolve, reject) => {// 创建读写事务const transaction = db.transaction([storeName], 'readwrite');const store = transaction.objectStore(storeName);// 添加或更新记录const request = store.put(item);// 处理结果request.onsuccess = () => resolve(request.result);request.onerror = () => reject(request.error);// 事务完成处理transaction.oncomplete = () => console.log('事务完成');transaction.onerror = event => console.error('事务错误:', event.target.error);});
}
获取数据
function getItem(db, storeName, key) {return new Promise((resolve, reject) => {const transaction = db.transaction([storeName], 'readonly');const store = transaction.objectStore(storeName);const request = store.get(key);request.onsuccess = () => resolve(request.result);request.onerror = () => reject(request.error);});
}
查询数据
function queryByIndex(db, storeName, indexName, value) {return new Promise((resolve, reject) => {const transaction = db.transaction([storeName], 'readonly');const store = transaction.objectStore(storeName);const index = store.index(indexName);const request = index.getAll(value);request.onsuccess = () => resolve(request.result);request.onerror = () => reject(request.error);});
}
使用游标遍历数据
function iterateWithCursor(db, storeName, callback) {return new Promise((resolve, reject) => {const transaction = db.transaction([storeName], 'readonly');const store = transaction.objectStore(storeName);const results = [

相关文章:

JavaScript性能优化实战(8):缓存策略与离线优化

前言 在Web应用中,性能优化不仅仅是关于代码执行速度,还与资源获取和数据持久化密切相关。合理的缓存策略可以显著减少网络请求,提升应用响应速度,同时有效降低服务器负载和用户流量消耗。离线优化则进一步解决了网络不稳定或断网场景下的用户体验问题,为Web应用提供类似…...

quantization-大模型权重量化简介

原文地址 https://towardsdatascience.com/introduction-to-weight-quantization-2494701b9c0c/ https://towardsdatascience.com/4-bit-quantization-with-gptq-36b0f4f02c34/ 权重量化简介 大型语言模型(LLM) 以其庞大的计算需求而闻名。通常&#xff0c;模型的大小是通过将参…...

unity ScriptObject的使用

1.先定义一个类数据类型 [Serializable] public class FoodItemData { public int foodID; // 食物唯一ID public string foodName; // 食物名称 [TextArea(3, 10)] // 多行文本输入 public string description; // 食物描述 pu…...

广义线性模型三剑客:线性回归、逻辑回归与Softmax分类的统一视角

文章目录 广义线性模型三剑客&#xff1a;线性回归、逻辑回归与Softmax分类的统一视角引言&#xff1a;机器学习中的"家族相似性"广义线性模型(GLMs)基础三位家族成员的统一视角1. 线性回归(Linear Regression)2. 逻辑回归(Logistic Regression)3. Softmax分类(Softm…...

Linux时钟与时间API

深入理解 Linux 时钟与时间 API 时间是计算领域的基础概念之一。在 Linux 系统中&#xff0c;精确可靠的时间管理对于系统日志记录、任务调度、网络通信、性能分析、文件系统操作乃至应用程序的正确运行都至关重要。本文将深入探讨 Linux 中的时钟类型、相关的 C API、使用示例…...

闭包(Closure)及其作用和影响

一、闭包是什么 闭包&#xff08;Closure&#xff09;指的是​​一个函数能够记住并访问其词法作用域&#xff08;lexical scope&#xff09;&#xff0c;即使该函数在其词法作用域之外执行​​。换句话说&#xff0c;闭包让函数可以“记住”它被创建时的环境。 闭包的核心特…...

toLua笔记

基本 LuaState luaStatenew LuaState(); luaState.Start(); luaState.DoString("xxx"); luaState.DoFile("yyy.lua"); luaState.Require("zzz");//不要加.lua后缀 luaState.CheckTop();//检查解析器栈顶为空 luaState.Dispose(); luaStatenull;…...

20:深度学习-多层感知器原理

深度学习-多层感知器的原理 ------------------常州龙熙机器视觉培训班-课程资料 1.单层感知机 多层感知机是由感知机推广而来&#xff0c;感知机学习算法(PLA: Perceptron Learning Algorithm)用神经元的结构进行描述的话就是一个单独的。 首先了解下单层感知机: b--常量 …...

高频数据冲击数据库的技术解析与应对方案

目录 前言一、问题现象与影响分析1.1 典型场景表现1.2 核心问题分类 二、失效根源深度剖析2.1 架构设计缺陷2.2 缓存策略缺陷 三、解决方案与最佳实践3.1 缓存架构设计3.1.1 分层缓存架构3.1.2 热点数据识别 3.2 缓存策略优化3.2.1 动态过期时间算法3.2.2 缓存更新策略对比 3.3…...

(37)VTK C++开发示例 ---纹理地球

文章目录 1. 概述2. CMake链接VTK3. main.cpp文件4. 演示效果 更多精彩内容&#x1f449;内容导航 &#x1f448;&#x1f449;VTK开发 &#x1f448; 1. 概述 将图片纹理贴到球体上&#xff0c;实现3D地球的效果。 该代码使用了 VTK (Visualization Toolkit) 库来创建一个纹理…...

LeetCode - 1137.第N个泰波那契数

目录 题目 解法 动态规划解法 核心思想 执行流程 具体例子 时间复杂度和空间复杂度 代码 题目 1137. 第 N 个泰波那契数 - 力扣&#xff08;LeetCode&#xff09; 解法 动态规划解法 核心思想 动态规划是一种通过将复杂问题分解为更小子问题来解决的算法方法。我将…...

智能决策支持系统的系统结构:四库架构与融合范式

前文我们已经了解了智能决策支持系统的基本概念以及基本构件&#xff0c;接下来我们了解一下系统结构。 有关“智能决策支持系统的基本概念”的内容&#xff0c;可看我文章&#xff1a;智能决策支持系统的基本概念与理论体系-CSDN博客 有关“智能决策支持系统的基本构建”的…...

单片机裸机环境下临界区保护

目录 1、直接中断屏蔽法 2、嵌套计数优化法 3、BASEPRI寄存器应用 4、动态优先级调整策略 5、LDREX/STREX指令应用 6、位带别名区原子访问 7、上下文感知保护 8、中断延迟优化技术 在嵌入式系统开发中&#xff0c;临界区保护是确保系统可靠性的关键技术。本文以ARM Cor…...

【数字电路】第六章 时序逻辑电路

一、时序逻辑电路概述 1.逻辑电路的分类 2.时序逻辑电路的一般结构形式 3.时序逻辑电路的描述方法 4.时序逻辑电路按触发器动作特点分类 5.时序逻辑电路按输出信号特点分类 6.常用时序逻辑电路 二、同步时序逻辑电路的分析 1.同步时序逻辑电路的分析方法 TTL触发器允许输入端…...

Spring Boot的GraalVM支持:构建低资源消耗微服务

文章目录 引言一、GraalVM原生镜像技术概述二、Spring Boot 3.x的GraalVM支持三、适配GraalVM的关键技术点四、构建原生镜像微服务实例五、性能优化与最佳实践总结 引言 微服务架构已成为企业应用开发的主流模式&#xff0c;但随着微服务数量的增加&#xff0c;资源消耗问题日…...

MySQL中的窗口函数

深入理解窗口函数&#xff08;Window Functions&#xff09; 窗口函数确实经常用于分组后为行分配序号&#xff08;如1,2,3…&#xff09;&#xff0c;但它的功能远不止于此。窗口函数是SQL中极其强大的分析工具&#xff0c;可以让你在不减少行数的情况下进行复杂计算。 窗口函…...

WITH在MYSQL中的用法

WITH 子句&#xff08;也称为公共表表达式&#xff0c;Common Table Expression&#xff0c;简称 CTE&#xff09;是 SQL 中一种强大的查询构建工具&#xff0c;它可以显著提高复杂查询的可读性和可维护性。 一、基本语法结构 WITH cte_name AS (SELECT ... -- 定义CTE的查询…...

人工智能:如何快速筛选出excel中某列存在跳号的单元格位置?

前提&#xff1a; 电脑上必须提前安装好了【office AI】软件工具 方法如下&#xff1a; 1、打开要操作的excel表格&#xff0c;点击上方的【officeAI】&#xff0c;再点击左边的【右侧面板】按钮&#xff0c;就会出现如下右侧的【OfficeAI助手】 2、在OfficeAI助手的聊天框…...

动态功耗与静态功耗

0 英文缩写 SOI&#xff08;Silicon on Insulator&#xff09;绝缘体上硅FET&#xff08;Field-Effect Transistor&#xff09;场效应管CMOS&#xff08;Complementary Metal Oxide Semiconductor&#xff09;互补金属氧化物半导体 1 功耗分类 CMOS电路功耗主要可以通过如下…...

Webug4.0靶场通关笔记10- 第14关链接注入

目录 第14关 链接注入 1.打开靶场 2.源码分析 3.渗透实战 &#xff08;1&#xff09;方法1&#xff1a;跳转外部网页 &#xff08;2&#xff09;方法2&#xff1a;获取cookie 4.漏洞防御 本文通过《webug靶场第14关 链接注入》来进行渗透实战。 第14关 链接注入 链接注…...

PyTorch_指定运算设备 (包含安装 GPU 的 PyTorch)

PyTorch默认会将张量创建在 CPU 控制的内存中&#xff0c;即&#xff1a;默认的运算设备为 CPU。我们也可以将张量创建在 GPU 上&#xff0c;能够利用对于矩阵计算的优势加快模型训练。 将张量移动到 GPU 上有两种方法&#xff1a; 使用 cuda 方法直接在 GPU 上创建张量使用 …...

Pytorch-CUDA版本环境配置

Pytorch-CUDA版本环境配置 电脑如果是Windows平台下的Nvidia GPU的用户&#xff0c;需配置Pytorch的CUDA版本&#xff0c;分为三步&#xff1a; 1. 安装或更新NVIDA显卡驱动 官方驱动下载地址&#xff1a; https://www.nvidia.cn/Download/index.aspx?langcn 2. 安装CUDA To…...

力扣:24两两交换链表的节点

目录 1.题目描述&#xff1a; 2.算法思路&#xff1a; 3.代码展示&#xff1a; 1.题目描述&#xff1a; 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能…...

SETNX的存在问题和redisson进行改进的原理

首先分布式锁的原理就是当锁不存在时则创建&#xff0c;创建到锁的线程则执行业务。但是在这些操作中会有一些问题&#xff0c;下面是redis命令setNX设置锁的代码片段 if(缓存中有){返回缓存中的数据 }else{获取分布式锁if(获取锁成功&#xff09;{try{查询数据库}finally{释放…...

抽象工厂模式(Abstract Factory Pattern)

很好&#xff01;你现在已经开始接触设计模式了&#xff0c;而**抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是一种常用于“创建一系列相关产品”**的经典设计模式。 我会一步步帮你理解&#xff1a; &#x1f9e0; 一句话解释 抽象工厂模式&#xff1a;提…...

AVIOContext 再学习

这个目前阶段用的不多&#xff0c;暂时不要花费太多精力。 url 的格式不同&#xff0c;使用的传输层协议也不同。这块看代码还没看到自己想的这样。 目前看的信息是&#xff1a;avformatContext 的 io_open 回调函数 在默认情况下叫 io_open_default&#xff0c;在解复用的 av…...

Power Query精通指南1:查询结构设计、数据类型、数据导入与迁移(平面文件、Excel、Web)

文章目录 零、Power Query简介0.1 Power Query 主要功能0.2 Power Query 的优势0.3 Power Query 组件 一、Power Query数据处理基本流程1.1 前期准备1.2 提取1.3 转换1.3.1 Power Query 编辑器界面1.3.2 默认转换1.3.3 自定义转换 1.4 加载1.4.1 自动检测数据类型1.4.2 重命名查…...

Linux 内核升级问题

一、内核升级后启动失败 原因&#xff1a;initramfs 镜像未正确生成或 GRUB 配置错误。 处理步骤如下&#xff1a; 1、进入旧内核启动系统。 2、重新生成 initramfs&#xff1a; sudo dracut -f --regenerate-all 3、更新 GRUB 配置&#xff1a; sudo grub2-mkconfig -o /boo…...

Linux 进程间通信(IPC)详解

进程间通信&#xff08;IPC&#xff09;深入解析 一、进程间通信概述 在操作系统里&#xff0c;不同进程间常常需要进行数据交换、同步协调等操作&#xff0c;进程间通信&#xff08;Inter - Process Communication&#xff0c;IPC&#xff09;机制应运而生。在Linux系统中&a…...

第3章 Python 3 基础语法001

文章目录 一、缩进规则1. 基本规则2. 示例3. 多级缩进4. 常见错误二、注释规则1. 单行注释2. 多行注释3. 特殊注释4. 注释规范三、代码块规则1. 控制结构2. 函数定义3. 类定义4. 上下文管理器四、总结与最佳实践五、调试技巧以下是 Python 3 基础语法规则的详细说明,涵盖 缩进…...

数据库介绍以及windows下mysql安装

文章目录 1. 前言2. MySQL概述2.1 相关概念2.2 DBMS的分类2.3 数据库交互图2.4 MySQL 介绍 3. MySQL 安装 数据库介绍以及windows下mysql安装 1. 前言 我们浏览的淘宝商品页面详情、刷视频网站的一个个视频&#xff0c;这些数据其实都是存储在公司的存储系统中的。想象一下&…...

list的两种设计

1. 内存布局对比 (1) MSVC 的实现 cpp class _List_node {_List_node* _Next; // 指向下一个节点_List_node* _Prev; // 指向前一个节点_Value_type _Value; // 存储的数据 }; 特点&#xff1a; 每个节点包含两个指针和一个数据成员。 Debug 模式&#xff1a;可能添加迭代…...

【C#】一个类中的接口方法使用static和不使用static的区别

在C#中&#xff0c;类中的接口方法是否使用 static 修饰符会带来显著的区别。这是因为接口方法的实现和调用方式与普通方法不同&#xff0c;而 static 关键字的使用进一步改变了这些行为。 以下是两者的区别&#xff1a; 1. 不使用 static 的接口方法 在这种情况下&#xff0…...

共铸价值:RWA 联合曲线价值模型,撬动现实资产生态

摘要 本文提出了一种针对真实资产&#xff08;RWA&#xff09;产业的联合曲线激励模型&#xff0c;将劳动与数据贡献映射为曲线价值&#xff0c;并基于固定档位与指数衰减奖励发放总计 2.1亿积分。该模型结合了去中心化定价与平滑递减机制&#xff0c;不仅为早期贡献者提供更高…...

【libuv】基于libuv的exe链接错误

vs2017构建 基于libuv的exe链接错误 1>libuv.lib(util.obj) : error LNK2019: unresolved external symbol __imp__GetAdaptersAddresses20 referenced in function _uv_interface_addresses 1>libuv.lib(util.obj) : error LNK2019: unresolved external symbol __imp__…...

什么是生成式 AI (GenAI)?

在科技飞速发展的今天,人工智能(AI)已不再是一个遥远的概念,而是悄然融入了我们的日常生活。从智能语音助手到自动驾驶汽车,从个性化推荐系统到医疗诊断辅助,AI正以前所未有的速度改变着世界。然而,在AI的广阔领域中,有一个分支正逐渐崭露头角,成为推动未来创新的关键…...

爬虫准备前工作

1.Pycham的下载 网址&#xff1a;PyCharm: The only Python IDE you need 2.Python的下载 网址&#xff1a;python.org&#xff08;python3.9版本之后都可以&#xff09; 3.node.js的下载 网址&#xff1a;Node.js — 在任何地方运行 JavaScript&#xff08;版本使用18就可…...

JVM——JVM 是如何处理异常的?

JVM 是如何处理异常的&#xff1f; 在 Java 编程语言中&#xff0c;异常处理是一种强大的机制&#xff0c;用于应对程序运行时出现的错误和意外情况。而 Java 虚拟机&#xff08;JVM&#xff09;作为 Java 程序运行的核心环境&#xff0c;在异常处理过程中扮演着至关重要的角色…...

网络基础-----C语言经典题目(12)

一、MTU&#xff0c;IP 协议头中 TTL是什么&#xff1f; MTU 指的是网络层能够接收的最大数据包大小&#xff0c;单位为字节。主要作用是限制数据链路层一次能够传输的数据量。 IP 协议头中的 TTL 是 IP 数据头部的一个 8 位字段&#xff0c;最初它的设计目的是限制数据包在网络…...

【第十六届蓝桥杯省赛】比赛心得与经验分享(PythonA 组)

文章目录 一、我的成绩二、我的备赛经历三、如何备赛&#xff08;个人观点&#xff09;1. 基础语法2. 数据结构3. 算法4. 数学 四、做题技巧与注意事项五、我的题解试题A 偏蓝 &#x1f3c6;100%试题B IPV6 &#x1f3c6;0%试题C 2025图形 &#x1f3c6;100%试题D 最大数字 &am…...

解决Maven项目中报错“java不支持版本6即更高的版本 7”

错误背景 当Maven项目编译或运行时出现错误提示 Java不支持版本6即更高的版本7&#xff0c;通常是由于项目配置的JDK版本与当前环境或编译器设置不一致导致的。例如&#xff1a; 项目配置的Java版本为6或7&#xff0c;但实际使用的是JDK 17。Maven或IDE的编译器未正确指定目标…...

MySQL--索引入门

MySQL官方对索引的定义为&#xff1a;索引&#xff08;Index&#xff09;是帮助MySQL高效获取数据的数据结构。 Mysql在存储数据之外&#xff0c;数据库系统各种还维护着满足特定查找算法的数据结构&#xff0c;这些数据结构以某种引用&#xff08;指向&#xff09;表中的数据…...

【网络原理】深入理解HTTPS协议

本篇博客给大家带来的是网络原理的知识点, 由于时间有限, 分三天来写, 本篇为线程第三篇,也是最后一篇. &#x1f40e;文章专栏: JavaEE初阶 &#x1f680;若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动…...

利用Elixir中的原子特性 + 错误消息泄露 -- Atom Bomb

题目信息: This new atom bomb early warning system is quite strange… 题目使用 elixir 语言 一开始,我们会访问 /page.html <!DOCTYPE html> <!-- 设定文档语言为英语 --> <html lang"en"> <head><!-- 设定字符编码为UTF-8 --><…...

机器人--STM32

STM32启动模式 1,从主闪存存储启动器启动(默认) 2,从系统存储启动器启动 下载程序时需要使用的启动方式。 3&#xff0c;从内置的SRAM启动...

LVGL -文本显示 英文、中文

1 文本 在 LVGL 中,文本控件(Label)是一种基本的 UI 组件,用于显示文本信息。文本控件可以用于各种场景,如显示状态信息、提示消息、标题等。在图形用户界面(GUI)开发中,文本是传达信息和指导用户的重要组成部分。为了有效地展示文本,以下是与文本相关的几个关键方面…...

Java面试资源获取

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 **1. GitHub开源项目****2. 技术博客与社区*…...

探索 Spring AI 的 ChatClient API:构建智能对话应用的利器

探索 Spring AI 的 ChatClient API&#xff1a;构建智能对话应用的利器 前言 在当今人工智能蓬勃发展的时代&#xff0c;智能对话系统成为了众多应用的核心组成部分。无论是客服机器人、智能助手还是聊天应用&#xff0c;都离不开高效、灵活的对话处理能力。Spring AI 作为 S…...

Java大师成长计划之第11天:Java Memory Model与Volatile关键字

&#x1f4e2; 友情提示&#xff1a; 本文由银河易创AI&#xff08;https://ai.eaigx.com&#xff09;平台gpt-4o-mini模型辅助创作完成&#xff0c;旨在提供灵感参考与技术分享&#xff0c;文中关键数据、代码与结论建议通过官方渠道验证。 在多线程编程中&#xff0c;线程的执…...

java学习之数据结构:一、数组

主要是对数组所有的东西进行总结&#xff0c;整理 适合小白~ 目录 1.什么是数组 1.1数组定义 1.2数组创建 1&#xff09;静态创建 2&#xff09;动态创建 1.3数组遍历 1&#xff09;for和while遍历 2&#xff09;foreach遍历 2.数组越界问题及解决 2.1数组越界问题 2…...