✨ Features: - App iOS completa para leer manga sin publicidad - Scraper con WKWebView para manhwaweb.com - Sistema de descargas offline - Lector con zoom y navegación - Favoritos y progreso de lectura - Compatible con iOS 15+ y Sideloadly/3uTools 📦 Contenido: - Backend Node.js con Puppeteer (opcional) - App iOS con SwiftUI - Scraper de capítulos e imágenes - Sistema de almacenamiento local - Testing completo - Documentación exhaustiva 🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente - 21 páginas descargadas - 4.68 MB total - URLs verificadas y funcionales 🎉 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
15 KiB
15 KiB
Quick Implementation Guide - MangaReader Optimizations
📂 Archivos Creados
ios-app/Sources/Services/
├── ManhwaWebScraperOptimized.swift # Scraper con cache inteligente
├── StorageServiceOptimized.swift # Storage con compresión y thumbnails
├── ImageCache.swift # Sistema de cache de imágenes
└── CacheManager.swift # Gerente central de cache
ios-app/Sources/Views/
└── ReaderViewOptimized.swift # Reader optimizado
OPTIMIZATION_SUMMARY.md # Documentación completa
IMPLEMENTATION_GUIDE.md # Este archivo
🔄 Migración Rápida
Opción 1: Reemplazo Directo (Recomendado para Testing)
// 1. En tus ViewModels, cambia:
private let scraper = ManhwaWebScraper.shared
// Por:
private let scraper = ManhwaWebScraperOptimized.shared
// 2. En tus Views, cambia:
private let storage = StorageService.shared
// Por:
private let storage = StorageServiceOptimized.shared
// 3. En ReaderView:
// Reemplaza completamente por ReaderViewOptimized
Opción 2: Migración Gradual (Más Segura)
// Usa alias de tipo para migrar gradualmente
typealias Scraper = ManhwaWebScraperOptimized
typealias Storage = StorageServiceOptimized
// Código existente funciona sin cambios
private let scraper = Scraper.shared
private let storage = Storage.shared
🔧 Configuración Inicial
1. Inicializar CacheManager en AppDelegate
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions ...
) -> Bool {
// Inicializar cache manager
let cacheManager = CacheManager.shared
#if DEBUG
// Imprimir reporte inicial en debug
cacheManager.printCacheReport()
#endif
return true
}
// Monitoring de background
func applicationDidEnterBackground(_ application: UIApplication) {
// El CacheManager ya maneja esto automáticamente
// pero puedes agregar lógica adicional aquí
}
// Monitoring de memory warnings
func applicationDidReceiveMemoryWarning(_ application: UIApplication) {
// CacheManager ya responde automáticamente
print("⚠️ Memory warning received")
}
}
2. Configurar Ajustes de Cache
// En algún lugar de configuración (ej: SettingsViewModel)
struct CacheSettings {
// Ajusta según tus necesidades
// Tamaño máximo de cache (en bytes)
static let maxImageCacheSize: Int64 = 500 * 1024 * 1024 // 500 MB
static let maxMetadataCacheSize: Int64 = 50 * 1024 * 1024 // 50 MB
// Tiempos de expiración (en segundos)
static let htmlCacheDuration: TimeInterval = 1800 // 30 min
static let imageCacheDuration: TimeInterval = 7 * 86400 // 7 días
static let thumbnailCacheDuration: TimeInterval = 30 * 86400 // 30 días
// Preloading
static let preloadAdjacentPages: Int = 2 // 2 páginas antes/después
static let enablePreloading: Bool = true
}
📊 Monitoreo y Debugging
Ver Estadísticas en Debug
#if DEBUG
// Agregar botón de debug en settings
struct DebugSettingsView: View {
var body: some View {
VStack {
Button("Print Cache Report") {
CacheManager.shared.printCacheReport()
}
Button("Print Image Statistics") {
ImageCache.shared.printStatistics()
}
Button("Clear All Cache") {
CacheManager.shared.clearAllCache()
}
Button("Simulate Memory Warning") {
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
}
}
}
#endif
Instruments Configuration
# Para perfilado de memoria
1. Xcode → Product → Profile
2. Seleccionar "Allocations"
3. Monitorear:
- Overall memory usage
- Anonymous VM
- Image cache size
# Para perfilado de tiempo
1. Seleccionar "Time Profiler"
2. Buscar funciones lentas
3. Verificar que cache esté funcionando
🧪 Testing Checklist
Tests Funcionales
class CachePerformanceTests: XCTestCase {
func testScraperCache() async throws {
let scraper = ManhwaWebScraperOptimized.shared
// Primer scraping (debe ser lento)
let start1 = Date()
_ = try await scraper.scrapeChapters(mangaSlug: "test-manga")
let time1 = Date().timeIntervalSince(start1)
// Segundo scraping (debe ser rápido por cache)
let start2 = Date()
_ = try await scraper.scrapeChapters(mangaSlug: "test-manga")
let time2 = Date().timeIntervalSince(start2)
// El segundo debe ser >10x más rápido
XCTAssertLessThan(time2, time1 / 10)
}
func testImageCompression() {
let storage = StorageServiceOptimized.shared
// Crear imagen de prueba grande (4 MB)
let largeImage = createTestImage(size: CGSize(width: 2000, height: 3000))
let expectation = XCTestExpectation(description: "Compress and save")
Task {
let url = try await storage.saveImage(
largeImage,
mangaSlug: "test",
chapterNumber: 1,
pageIndex: 0
)
// Verificar que archivo resultante es < 2 MB
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
let fileSize = attributes[.size] as! Int64
XCTAssertLessThan(fileSize, 2 * 1024 * 1024)
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
}
func testPreloading() {
let viewModel = ReaderViewModelOptimized(
manga: testManga,
chapter: testChapter
)
// Cargar páginas
let expectation = XCTestExpectation(description: "Load pages")
Task {
await viewModel.loadPages()
expectation.fulfill()
}
wait(for: [expectation], timeout: 10.0)
// Simular navegación a página 5
viewModel.currentPage = 5
// Verificar que páginas 3, 4, 6, 7 están en cache
let imageCache = ImageCache.shared
XCTAssertNotNil(imageCache.image(for: viewModel.pages[3].url))
XCTAssertNotNil(imageCache.image(for: viewModel.pages[4].url))
XCTAssertNotNil(imageCache.image(for: viewModel.pages[6].url))
XCTAssertNotNil(imageCache.image(for: viewModel.pages[7].url))
}
}
Tests de Memoria
class MemoryTests: XCTestCase {
func testMemoryUnderLoad() {
let viewModel = ReaderViewModelOptimized(
manga: largeManga, // 100 páginas
chapter: largeChapter
)
let memoryBefore = getMemoryUsage()
let expectation = XCTestExpectation(description: "Load all pages")
Task {
await viewModel.loadPages()
// Navegar por todas las páginas
for i in 0..<viewModel.pages.count {
viewModel.currentPage = i
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s
}
expectation.fulfill()
}
wait(for: [expectation], timeout: 60.0)
let memoryAfter = getMemoryUsage()
let memoryIncrease = memoryAfter - memoryBefore
// No debe aumentar más de 150 MB
XCTAssertLessThan(memoryIncrease, 150 * 1024 * 1024)
}
func testMemoryWarningResponse() {
// Llenar cache
let cache = ImageCache.shared
for i in 0..<100 {
cache.setImage(createTestImage(), for: "test_\(i)")
}
let itemsBefore = cache.getCacheStatistics().memoryCacheHits
// Simular memory warning
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
// Esperar un poco
Thread.sleep(forTimeInterval: 0.5)
// Cache debe estar vacío o casi vacío
let itemsAfter = cache.getCacheStatistics().memoryCacheHits
XCTAssertLessThan(itemsAfter, itemsBefore / 2)
}
private func getMemoryUsage() -> UInt64 {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? info.resident_size : 0
}
}
🎯 Optimizaciones por Categoría
Scraper Optimizations
| Optimización | Archivo | Líneas Clave |
|---|---|---|
| WKWebView reuse | ManhwaWebScraperOptimized.swift |
30-45 |
| HTML cache | ManhwaWebScraperOptimized.swift |
48-62 |
| Precompiled JS | ManhwaWebScraperOptimized.swift |
67-122 |
| Adaptive timeout | ManhwaWebScraperOptimized.swift |
127-165 |
| Concurrency control | ManhwaWebScraperOptimized.swift |
170-195 |
Storage Optimizations
| Optimización | Archivo | Líneas Clave |
|---|---|---|
| Adaptive compression | StorageServiceOptimized.swift |
28-52 |
| Thumbnail system | StorageServiceOptimized.swift |
57-85 |
| Lazy loading | StorageServiceOptimized.swift |
478-488 |
| Auto cleanup | StorageServiceOptimized.swift |
330-380 |
| Batch operations | StorageServiceOptimized.swift |
220-245 |
Reader Optimizations
| Optimización | Archivo | Líneas Clave |
|---|---|---|
| NSCache | ImageCache.swift |
22-45 |
| Preloading | ReaderViewOptimized.swift |
195-215 |
| Memory management | ReaderViewOptimized.swift |
420-445 |
| Debouncing | ReaderViewOptimized.swift |
505-525 |
🔍 Troubleshooting
Problema: Cache no funciona
// Verificar que el singleton se esté usando correctamente
let scraper = ManhwaWebScraperOptimized.shared
// NO: let scraper = ManhwaWebScraperOptimized()
// Verificar que los métodos tengan @MainActor si es necesario
@MainActor
func loadChapters() async throws {
// código
}
Problema: Memoria sigue alta
// 1. Verificar que se estén liberando referencias
deinit {
progressSaveTimer?.invalidate()
NotificationCenter.default.removeObserver(self)
}
// 2. Usizar weak en closures
Task { [weak self] in
await self?.loadPages()
}
// 3. Verificar en Instruments
// - Revisar "Anonymous VM" (debe ser bajo)
// - Buscar "Malloc" leaks
Problema: Preloading no funciona
// Verificar que esté habilitado
viewModel.enablePreloading = true // Debe ser true
// Verificar que onAppear se llame
PageView(page: page)
.onAppear {
print("Page \(page.index) appeared") // Debug
viewModel.preloadAdjacentPages(...)
}
// Verificar priority queue
Task(priority: .utility) {
// Debe usar .utility, no .background
}
Problema: Thumbnails no se generan
// Verificar directorio de thumbnails
let thumbDir = storage.getThumbnailDirectory(...)
print("Thumb dir: \(thumbDir.path)")
// Verificar que se cree el directorio
try? FileManager.default.createDirectory(
at: thumbDir,
withIntermediateDirectories: true
)
// Verificar que se guarde correctamente
try? thumbData.write(to: thumbnailURL)
📈 Métricas de Éxito
Objetivos de Rendimiento
struct PerformanceTargets {
// Tiempos de carga
static let maxChapterLoadTime: TimeInterval = 2.0 // segundos
static let maxPageLoadTime: TimeInterval = 0.5 // segundos (con cache)
static let maxAppLaunchTime: TimeInterval = 1.0 // segundos
// Memoria
static let maxReaderMemory: UInt64 = 120 * 1024 * 1024 // 120 MB
static let maxScraperMemory: UInt64 = 50 * 1024 * 1024 // 50 MB
// Cache
static let minCacheHitRate: Double = 0.80 // 80%
static let maxCacheSize: Int64 = 500 * 1024 * 1024 // 500 MB
}
// Función de verificación
func verifyPerformanceTargets() {
let cacheStats = ImageCache.shared.getCacheStatistics()
let hitRate = cacheStats.hitRate
XCTAssertGreaterThan(hitRate, PerformanceTargets.minCacheHitRate,
"Cache hit rate \(hitRate) below target")
// ... más verificaciones
}
🚀 Próximos Pasos
Implementación Inmediata
-
Backup del código existente
cp -r ios-app/Sources ios-app/Sources.backup -
Reemplazar archivos uno por uno
- Empezar con ManhwaWebScraperOptimized
- Probar exhaustivamente
- Continuar con StorageServiceOptimized
- etc.
-
Testing completo
- Unit tests
- Integration tests
- Manual testing
Optimizaciones Futuras (Opcionales)
-
Prefetching Predictivo
// Usar ML para predecir próximo capítulo func predictNextChapter(userHistory: [Chapter]) -> Chapter? { // Implementación básica de ML } -
Compresión HEIC
// Usar HEIC en lugar de JPEG (50% más eficiente) if #available(iOS 11.0, *) { let heicData = image.heicData() } -
Progressive Image Loading
// Cargar versión baja resolución primero // Luego reemplazar con alta resolución -
Background Sync
// Sincronizar en background usando BGTaskScheduler func scheduleBackgroundSync() { let request = BGAppRefreshTaskRequest(...) try? BGTaskScheduler.shared.submit(request) }
💡 Tips y Best Practices
DO ✅
// 1. Usar singletons para caches
static let shared = ImageCache()
// 2. Usar colas para I/O
private let ioQueue = DispatchQueue(label: "...", qos: .utility)
// 3. Weak references en closures
Task { [weak self] in
await self?.loadData()
}
// 4. Responder a memory warnings
@objc func handleMemoryWarning() {
cache.removeAllObjects()
}
// 5. Debouncing de operaciones frecuentes
func saveDebounced() {
timer?.invalidate()
timer = Timer.scheduledTimer(...)
}
DON'T ❌
// 1. NO crear múltiples instancias de cache
// let cache1 = ImageCache()
// let cache2 = ImageCache() // ❌
// 2. NO hacer I/O en main thread
// let data = try Data(contentsOf: url) // ❌ Bloquea
// 3. NO olvidar weak en closures
// Task {
// self.doSomething() // ❌ Memory leak
// }
// 4. NO ignorar memory warnings
// @objc func handleMemoryWarning() {
// // ❌ No hacer nada
// }
// 5. NO guardar en cada cambio
// func onChange() {
// saveToDisk() // ❌ Demasiado frecuente
// }
📞 Soporte
Si encuentras problemas:
- Revisa
OPTIMIZATION_SUMMARY.mdpara detalles técnicos - Usa los tests de ejemplo como guía
- Habilita logging debug en development
- Usa Instruments para perfilado
Happy Optimizing! 🚀