Performance Optimization - luckydeva03/barbershop_app GitHub Wiki
Panduan lengkap optimasi performa untuk barbershop management system agar berjalan cepat dan efisien.
Aspek optimasi yang dicakup:
- Database Optimization: Query optimization, indexing, caching
- Frontend Performance: Asset optimization, lazy loading, compression
- Server Optimization: Caching strategies, CDN integration
- Code Optimization: PHP optimization, memory management
- Monitoring: Performance tracking, profiling tools
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
// Users table indexes
Schema::table('users', function (Blueprint $table) {
$table->index(['email', 'is_active']);
$table->index(['last_login_at']);
$table->index(['created_at']);
$table->index(['email_verified_at']);
});
// History Points table indexes
Schema::table('history_points', function (Blueprint $table) {
$table->index(['user_id', 'type', 'created_at']);
$table->index(['expires_at']);
$table->index(['reference_code']);
$table->index(['created_at', 'type']);
});
// Reviews table indexes
Schema::table('reviews', function (Blueprint $table) {
$table->index(['store_id', 'created_at']);
$table->index(['user_id', 'created_at']);
$table->index(['rating']);
$table->index(['created_at']);
});
// Stores table indexes
Schema::table('stores', function (Blueprint $table) {
$table->index(['is_active']);
$table->index(['average_rating']);
$table->index(['created_at']);
$table->spatial(['latitude', 'longitude']);
});
// Redeem codes table indexes
Schema::table('reedem_codes', function (Blueprint $table) {
$table->index(['code', 'is_active']);
$table->index(['expires_at', 'is_active']);
$table->index(['created_at']);
});
// Security audit log indexes
Schema::table('security_audit_log', function (Blueprint $table) {
$table->index(['event', 'created_at']);
$table->index(['severity', 'created_at']);
$table->index(['user_id', 'created_at']);
$table->index(['ip_address', 'created_at']);
});
}
public function down()
{
// Drop indexes in reverse order
Schema::table('security_audit_log', function (Blueprint $table) {
$table->dropIndex(['event', 'created_at']);
$table->dropIndex(['severity', 'created_at']);
$table->dropIndex(['user_id', 'created_at']);
$table->dropIndex(['ip_address', 'created_at']);
});
Schema::table('reedem_codes', function (Blueprint $table) {
$table->dropIndex(['code', 'is_active']);
$table->dropIndex(['expires_at', 'is_active']);
$table->dropIndex(['created_at']);
});
Schema::table('stores', function (Blueprint $table) {
$table->dropIndex(['is_active']);
$table->dropIndex(['average_rating']);
$table->dropIndex(['created_at']);
$table->dropSpatialIndex(['latitude', 'longitude']);
});
Schema::table('reviews', function (Blueprint $table) {
$table->dropIndex(['store_id', 'created_at']);
$table->dropIndex(['user_id', 'created_at']);
$table->dropIndex(['rating']);
$table->dropIndex(['created_at']);
});
Schema::table('history_points', function (Blueprint $table) {
$table->dropIndex(['user_id', 'type', 'created_at']);
$table->dropIndex(['expires_at']);
$table->dropIndex(['reference_code']);
$table->dropIndex(['created_at', 'type']);
});
Schema::table('users', function (Blueprint $table) {
$table->dropIndex(['email', 'is_active']);
$table->dropIndex(['last_login_at']);
$table->dropIndex(['created_at']);
$table->dropIndex(['email_verified_at']);
});
}
};
<?php
namespace App\Repositories;
use App\Models\Store;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class OptimizedStoreRepository
{
protected $cacheTime = 3600; // 1 hour
/**
* Get stores with optimized queries
*/
public function getStoresWithStats(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
$cacheKey = 'stores_with_stats_' . md5(serialize($filters) . $perPage);
return Cache::remember($cacheKey, $this->cacheTime, function () use ($filters, $perPage) {
$query = Store::select([
'stores.*',
DB::raw('COUNT(reviews.id) as reviews_count'),
DB::raw('AVG(reviews.rating) as avg_rating')
])
->leftJoin('reviews', 'stores.id', '=', 'reviews.store_id')
->with(['reviews' => function ($query) {
$query->select('id', 'store_id', 'rating', 'created_at')
->latest()
->limit(3);
}])
->active()
->groupBy('stores.id');
// Apply filters
if (isset($filters['rating_min'])) {
$query->having('avg_rating', '>=', $filters['rating_min']);
}
if (isset($filters['has_reviews'])) {
$query->having('reviews_count', '>', 0);
}
if (isset($filters['location']) && isset($filters['radius'])) {
$query->whereRaw(
"ST_Distance_Sphere(POINT(longitude, latitude), POINT(?, ?)) <= ?",
[$filters['location']['lng'], $filters['location']['lat'], $filters['radius'] * 1000]
);
}
return $query->orderBy('avg_rating', 'desc')
->orderBy('reviews_count', 'desc')
->paginate($perPage);
});
}
/**
* Get popular stores (cached)
*/
public function getPopularStores(int $limit = 10): Collection
{
return Cache::remember('popular_stores_' . $limit, $this->cacheTime, function () use ($limit) {
return Store::select([
'id', 'name', 'average_rating', 'reviews_count', 'image_url'
])
->active()
->where('reviews_count', '>', 0)
->orderBy('average_rating', 'desc')
->orderBy('reviews_count', 'desc')
->limit($limit)
->get();
});
}
/**
* Get nearby stores with spatial index
*/
public function getNearbyStores(float $lat, float $lng, float $radius = 10, int $limit = 20): Collection
{
$cacheKey = "nearby_stores_{$lat}_{$lng}_{$radius}_{$limit}";
return Cache::remember($cacheKey, 1800, function () use ($lat, $lng, $radius, $limit) {
return Store::select([
'stores.*',
DB::raw("ST_Distance_Sphere(POINT(longitude, latitude), POINT(?, ?)) as distance")
])
->active()
->whereRaw(
"ST_Distance_Sphere(POINT(longitude, latitude), POINT(?, ?)) <= ?",
[$lng, $lat, $radius * 1000]
)
->setBindings([$lng, $lat, $lng, $lat])
->orderBy('distance')
->limit($limit)
->get();
});
}
/**
* Get store statistics (heavily cached)
*/
public function getStoreStatistics(): array
{
return Cache::remember('store_statistics', 7200, function () {
return [
'total_stores' => Store::count(),
'active_stores' => Store::active()->count(),
'avg_rating' => Store::active()->avg('average_rating'),
'total_reviews' => Store::sum('reviews_count'),
'top_rated' => Store::active()
->where('reviews_count', '>', 5)
->orderBy('average_rating', 'desc')
->first(['id', 'name', 'average_rating']),
'newest_stores' => Store::active()
->latest()
->limit(5)
->get(['id', 'name', 'created_at']),
];
});
}
/**
* Clear related caches
*/
public function clearStoreCache()
{
$patterns = [
'stores_with_stats_*',
'popular_stores_*',
'nearby_stores_*',
'store_statistics',
];
foreach ($patterns as $pattern) {
Cache::tags(['stores'])->flush();
}
}
}
<?php
return [
'default' => env('DB_CONNECTION', 'mysql'),
'connections' => [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => 'InnoDB',
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_TIMEOUT => 30,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
PDO::MYSQL_ATTR_LOCAL_INFILE => true,
]) : [],
],
// Read replica for heavy read operations
'mysql_read' => [
'driver' => 'mysql',
'read' => [
'host' => [
env('DB_READ_HOST_1', env('DB_HOST', '127.0.0.1')),
env('DB_READ_HOST_2', env('DB_HOST', '127.0.0.1')),
],
],
'write' => [
'host' => [
env('DB_WRITE_HOST', env('DB_HOST', '127.0.0.1')),
],
],
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => 'InnoDB',
],
],
'migrations' => 'migrations',
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'read_write_timeout' => 60,
'persistent' => true,
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'read_write_timeout' => 60,
'persistent' => true,
],
'session' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_SESSION_DB', '2'),
'read_write_timeout' => 60,
'persistent' => true,
],
],
];
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class CachingService
{
const SHORT_CACHE = 300; // 5 minutes
const MEDIUM_CACHE = 1800; // 30 minutes
const LONG_CACHE = 3600; // 1 hour
const DAILY_CACHE = 86400; // 24 hours
/**
* Multi-level cache with fallback
*/
public static function remember($key, $ttl, $callback, $tags = [])
{
// Try memory cache first (APCu)
if (function_exists('apcu_fetch') && apcu_exists($key)) {
return apcu_fetch($key);
}
// Try Redis cache
$value = Cache::tags($tags)->remember($key, $ttl, $callback);
// Store in memory cache for faster access
if (function_exists('apcu_store')) {
apcu_store($key, $value, min($ttl, 300)); // Max 5 minutes in memory
}
return $value;
}
/**
* Cache user-specific data
*/
public static function rememberForUser($userId, $key, $ttl, $callback)
{
$cacheKey = "user_{$userId}_{$key}";
return self::remember($cacheKey, $ttl, $callback, ['users', "user_{$userId}"]);
}
/**
* Cache store-specific data
*/
public static function rememberForStore($storeId, $key, $ttl, $callback)
{
$cacheKey = "store_{$storeId}_{$key}";
return self::remember($cacheKey, $ttl, $callback, ['stores', "store_{$storeId}"]);
}
/**
* Invalidate cache with patterns
*/
public static function forgetPattern($pattern)
{
// For Redis
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$keys = Redis::keys("*{$pattern}*");
if (!empty($keys)) {
Redis::del($keys);
}
}
// For APCu
if (function_exists('apcu_delete')) {
$info = apcu_cache_info();
foreach ($info['cache_list'] as $entry) {
if (strpos($entry['info'], $pattern) !== false) {
apcu_delete($entry['info']);
}
}
}
}
/**
* Clear user-specific cache
*/
public static function clearUserCache($userId)
{
Cache::tags(["user_{$userId}"])->flush();
self::forgetPattern("user_{$userId}_");
}
/**
* Clear store-specific cache
*/
public static function clearStoreCache($storeId)
{
Cache::tags(["store_{$storeId}"])->flush();
self::forgetPattern("store_{$storeId}_");
}
/**
* Warm up common caches
*/
public static function warmUpCache()
{
// Warm up popular stores
\DB::table('stores')
->select('id')
->active()
->orderBy('average_rating', 'desc')
->limit(20)
->get()
->each(function ($store) {
app(OptimizedStoreRepository::class)->getStoreDetails($store->id);
});
// Warm up statistics
app(OptimizedStoreRepository::class)->getStoreStatistics();
// Warm up popular locations
$popularLocations = [
['lat' => -6.2088, 'lng' => 106.8456], // Jakarta
['lat' => -6.9175, 'lng' => 107.6191], // Bandung
['lat' => -7.2575, 'lng' => 112.7521], // Surabaya
];
foreach ($popularLocations as $location) {
app(OptimizedStoreRepository::class)->getNearbyStores(
$location['lat'],
$location['lng']
);
}
}
/**
* Cache with compression for large data
*/
public static function rememberCompressed($key, $ttl, $callback, $tags = [])
{
return Cache::tags($tags)->remember($key, $ttl, function () use ($callback) {
$data = $callback();
return gzcompress(serialize($data), 9);
});
}
/**
* Retrieve compressed cache
*/
public static function getCompressed($key)
{
$compressed = Cache::get($key);
if ($compressed) {
return unserialize(gzuncompress($compressed));
}
return null;
}
}
<?php
namespace App\Traits;
use App\Services\CachingService;
trait Cacheable
{
/**
* Boot the cacheable trait
*/
public static function bootCacheable()
{
static::saved(function ($model) {
$model->clearModelCache();
});
static::deleted(function ($model) {
$model->clearModelCache();
});
}
/**
* Cache a model method result
*/
public function cacheResult($method, $ttl = 3600, ...$args)
{
$key = $this->getCacheKey($method, $args);
return CachingService::remember($key, $ttl, function () use ($method, $args) {
return $this->$method(...$args);
}, $this->getCacheTags());
}
/**
* Get cache key for model
*/
protected function getCacheKey($method, $args = [])
{
$class = class_basename($this);
$id = $this->getKey();
$argsHash = md5(serialize($args));
return strtolower("{$class}_{$id}_{$method}_{$argsHash}");
}
/**
* Get cache tags for model
*/
protected function getCacheTags()
{
$class = class_basename($this);
return [strtolower($class . 's'), strtolower($class . '_' . $this->getKey())];
}
/**
* Clear model-specific cache
*/
public function clearModelCache()
{
$tags = $this->getCacheTags();
\Cache::tags($tags)->flush();
}
/**
* Clear all model caches
*/
public static function clearAllModelCache()
{
$class = class_basename(static::class);
\Cache::tags([strtolower($class . 's')])->flush();
}
}
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { resolve } from 'path';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/js/admin.js',
'resources/js/charts.js',
],
refresh: true,
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunk for third-party libraries
vendor: [
'bootstrap',
'axios',
'lodash',
],
// Charts chunk for analytics
charts: [
'chart.js',
],
// Utils chunk for common utilities
utils: [
'./resources/js/utils.js',
],
},
},
},
// Compression and optimization
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
// CSS optimization
cssCodeSplit: true,
cssMinify: true,
// Asset optimization
assetsInlineLimit: 4096, // 4KB
// Source maps for production debugging
sourcemap: process.env.NODE_ENV === 'development',
},
server: {
hmr: {
host: 'localhost',
},
watch: {
usePolling: true,
},
},
resolve: {
alias: {
'@': resolve(__dirname, 'resources/js'),
'@css': resolve(__dirname, 'resources/css'),
},
},
// CSS preprocessing
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@css/variables.scss";`,
},
},
postcss: {
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: 'default',
}),
],
},
},
});
class LazyLoadService {
constructor() {
this.imageObserver = null;
this.contentObserver = null;
this.init();
}
init() {
if ('IntersectionObserver' in window) {
this.setupImageLazyLoading();
this.setupContentLazyLoading();
} else {
// Fallback for older browsers
this.loadAllImages();
}
}
setupImageLazyLoading() {
this.imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage(entry.target);
this.imageObserver.unobserve(entry.target);
}
});
}, {
root: null,
rootMargin: '50px',
threshold: 0.1
});
this.observeImages();
}
setupContentLazyLoading() {
this.contentObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadContent(entry.target);
this.contentObserver.unobserve(entry.target);
}
});
}, {
root: null,
rootMargin: '100px',
threshold: 0.1
});
this.observeContent();
}
observeImages() {
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach(img => {
this.imageObserver.observe(img);
});
}
observeContent() {
const lazyContent = document.querySelectorAll('[data-lazy-load]');
lazyContent.forEach(element => {
this.contentObserver.observe(element);
});
}
loadImage(img) {
// Show loading placeholder
img.classList.add('loading');
const image = new Image();
image.onload = () => {
img.src = img.dataset.src;
img.classList.remove('loading');
img.classList.add('loaded');
// Remove data-src to prevent reloading
delete img.dataset.src;
};
image.onerror = () => {
img.src = '/images/placeholder-error.png';
img.classList.remove('loading');
img.classList.add('error');
};
image.src = img.dataset.src;
}
loadContent(element) {
const url = element.dataset.lazyLoad;
const method = element.dataset.method || 'GET';
// Show loading state
element.innerHTML = '<div class="text-center p-4"><div class="spinner-border" role="status"></div></div>';
fetch(url, { method })
.then(response => response.text())
.then(html => {
element.innerHTML = html;
element.classList.add('lazy-loaded');
// Initialize any JavaScript components in the loaded content
this.initializeComponents(element);
})
.catch(error => {
element.innerHTML = '<div class="alert alert-warning">Failed to load content</div>';
console.error('Lazy load error:', error);
});
}
initializeComponents(container) {
// Reinitialize tooltips
const tooltips = container.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach(el => new bootstrap.Tooltip(el));
// Reinitialize any other components
if (window.App && window.App.reinitialize) {
window.App.reinitialize(container);
}
}
loadAllImages() {
// Fallback for browsers without IntersectionObserver
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach(img => {
img.src = img.dataset.src;
delete img.dataset.src;
});
}
// Public methods for dynamic content
observeNewImages(container = document) {
if (this.imageObserver) {
const newImages = container.querySelectorAll('img[data-src]');
newImages.forEach(img => this.imageObserver.observe(img));
}
}
observeNewContent(container = document) {
if (this.contentObserver) {
const newContent = container.querySelectorAll('[data-lazy-load]');
newContent.forEach(element => this.contentObserver.observe(element));
}
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.LazyLoadService = new LazyLoadService();
});
export default LazyLoadService;
<?php
namespace App\Services;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Facades\Storage;
class ImageOptimizationService
{
const SIZES = [
'thumbnail' => [150, 150],
'small' => [300, 300],
'medium' => [600, 600],
'large' => [1200, 1200],
];
const QUALITY = [
'thumbnail' => 70,
'small' => 75,
'medium' => 80,
'large' => 85,
];
/**
* Optimize and create multiple sizes of an image
*/
public static function optimizeAndResize($imagePath, $outputDir = 'optimized')
{
$results = [];
$originalImage = Image::make(Storage::path($imagePath));
foreach (self::SIZES as $sizeName => [$width, $height]) {
$optimizedImage = clone $originalImage;
// Resize with aspect ratio
$optimizedImage->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
// Add padding if needed to exact dimensions
$optimizedImage->resizeCanvas($width, $height, 'center', false, 'ffffff');
// Set quality
$quality = self::QUALITY[$sizeName] ?? 80;
// Generate filename
$pathInfo = pathinfo($imagePath);
$filename = $pathInfo['filename'] . "_{$sizeName}." . $pathInfo['extension'];
$outputPath = $outputDir . '/' . $filename;
// Save optimized image
$optimizedImage->encode($pathInfo['extension'], $quality);
Storage::put($outputPath, $optimizedImage->getEncoded());
$results[$sizeName] = $outputPath;
}
// Create WebP versions for modern browsers
foreach ($results as $sizeName => $path) {
$webpPath = str_replace('.jpg', '.webp', str_replace('.png', '.webp', $path));
$image = Image::make(Storage::path($path));
$image->encode('webp', self::QUALITY[$sizeName] ?? 80);
Storage::put($webpPath, $image->getEncoded());
$results[$sizeName . '_webp'] = $webpPath;
}
return $results;
}
/**
* Generate responsive image HTML
*/
public static function generateResponsiveImageHtml($basePath, $alt = '', $class = '', $lazy = true)
{
$baseUrl = Storage::url('');
$pathInfo = pathinfo($basePath);
$baseFilename = $pathInfo['filename'];
$extension = $pathInfo['extension'];
// Build srcset for different sizes
$srcset = [];
$webpSrcset = [];
foreach (self::SIZES as $sizeName => [$width, $height]) {
$filename = "{$baseFilename}_{$sizeName}.{$extension}";
$webpFilename = "{$baseFilename}_{$sizeName}.webp";
$srcset[] = "{$baseUrl}{$filename} {$width}w";
$webpSrcset[] = "{$baseUrl}{$webpFilename} {$width}w";
}
$srcsetStr = implode(', ', $srcset);
$webpSrcsetStr = implode(', ', $webpSrcset);
// Default image (medium size)
$defaultImage = "{$baseUrl}{$baseFilename}_medium.{$extension}";
$lazyAttr = $lazy ? 'loading="lazy"' : '';
$dataAttr = $lazy ? 'data-src' : 'src';
return <<<HTML
<picture>
<source type="image/webp" {$dataAttr}set="{$webpSrcsetStr}" sizes="(max-width: 300px) 300px, (max-width: 600px) 600px, 1200px">
<img {$dataAttr}="{$defaultImage}"
srcset="{$srcsetStr}"
sizes="(max-width: 300px) 300px, (max-width: 600px) 600px, 1200px"
alt="{$alt}"
class="{$class}"
{$lazyAttr}>
</picture>
HTML;
}
/**
* Clean up old optimized images
*/
public static function cleanupOldImages($directory = 'optimized', $olderThanDays = 30)
{
$files = Storage::allFiles($directory);
$cutoffDate = now()->subDays($olderThanDays);
$deletedCount = 0;
foreach ($files as $file) {
$lastModified = Storage::lastModified($file);
if ($lastModified < $cutoffDate->timestamp) {
Storage::delete($file);
$deletedCount++;
}
}
return $deletedCount;
}
}
<?php
// Add to existing config/app.php
/*
|--------------------------------------------------------------------------
| Performance Configuration
|--------------------------------------------------------------------------
*/
'performance' => [
'opcache' => [
'enabled' => env('OPCACHE_ENABLED', true),
'memory_consumption' => env('OPCACHE_MEMORY', 256),
'max_accelerated_files' => env('OPCACHE_MAX_FILES', 20000),
],
'session' => [
'gc_probability' => env('SESSION_GC_PROBABILITY', 1),
'gc_divisor' => env('SESSION_GC_DIVISOR', 1000),
'gc_maxlifetime' => env('SESSION_GC_MAXLIFETIME', 7200),
],
'memory' => [
'limit' => env('MEMORY_LIMIT', '256M'),
'max_execution_time' => env('MAX_EXECUTION_TIME', 60),
],
],
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CompressResponse
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
// Only compress if client accepts gzip
if (!$this->shouldCompress($request, $response)) {
return $response;
}
$content = $response->getContent();
if (strlen($content) > 1024) { // Only compress if content > 1KB
$compressed = gzencode($content, 6); // Compression level 6 (good balance)
if ($compressed !== false && strlen($compressed) < strlen($content)) {
$response->setContent($compressed);
$response->headers->set('Content-Encoding', 'gzip');
$response->headers->set('Content-Length', strlen($compressed));
$response->headers->set('Vary', 'Accept-Encoding');
}
}
return $response;
}
protected function shouldCompress(Request $request, $response)
{
// Check if client accepts gzip
if (!str_contains($request->header('Accept-Encoding', ''), 'gzip')) {
return false;
}
// Don't compress if already compressed
if ($response->headers->has('Content-Encoding')) {
return false;
}
// Only compress certain content types
$contentType = $response->headers->get('Content-Type', '');
$compressibleTypes = [
'text/html',
'text/css',
'text/javascript',
'application/javascript',
'application/json',
'text/xml',
'application/xml',
];
foreach ($compressibleTypes as $type) {
if (str_contains($contentType, $type)) {
return true;
}
}
return false;
}
}
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
class OptimizeAssets extends Command
{
protected $signature = 'assets:optimize {--force : Force regeneration of optimized assets}';
protected $description = 'Optimize static assets for production';
public function handle()
{
$this->info('🚀 Starting asset optimization...');
// Optimize images
$this->optimizeImages();
// Minify CSS/JS (if not using Vite)
$this->minifyAssets();
// Generate service worker
$this->generateServiceWorker();
// Generate manifest
$this->generateManifest();
$this->info('✅ Asset optimization completed!');
return 0;
}
protected function optimizeImages()
{
$this->info('📷 Optimizing images...');
$imagePaths = [
'public/images',
'storage/app/public/images',
];
foreach ($imagePaths as $path) {
if (File::isDirectory($path)) {
$files = File::allFiles($path);
foreach ($files as $file) {
if (in_array(strtolower($file->getExtension()), ['jpg', 'jpeg', 'png'])) {
$this->optimizeImage($file->getPathname());
}
}
}
}
}
protected function optimizeImage($path)
{
// Use ImageMagick or similar tool for optimization
$command = "jpegoptim --max=85 --strip-all {$path}";
if (strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'png') {
$command = "optipng -o7 {$path}";
}
exec($command);
}
protected function minifyAssets()
{
$this->info('📦 Minifying assets...');
// This is handled by Vite in modern setups
// But you can add custom minification here if needed
}
protected function generateServiceWorker()
{
$this->info('⚙️ Generating service worker...');
$serviceWorker = <<<JS
const CACHE_NAME = 'barbershop-v' + Date.now();
const STATIC_CACHE = 'barbershop-static-v1';
// Cache static assets
const STATIC_ASSETS = [
'/',
'/css/app.css',
'/js/app.js',
'/images/logo.png',
'/manifest.json'
];
// Install event
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(STATIC_ASSETS))
);
});
// Activate event
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME && cacheName !== STATIC_CACHE) {
return caches.delete(cacheName);
}
})
);
})
);
});
// Fetch event
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached version or fetch from network
return response || fetch(event.request);
})
);
});
JS;
File::put(public_path('sw.js'), $serviceWorker);
}
protected function generateManifest()
{
$this->info('📱 Generating manifest...');
$manifest = [
'name' => config('app.name'),
'short_name' => 'Barbershop',
'description' => 'Barbershop Management System',
'start_url' => '/',
'display' => 'standalone',
'background_color' => '#ffffff',
'theme_color' => '#007bff',
'icons' => [
[
'src' => '/images/icon-192.png',
'sizes' => '192x192',
'type' => 'image/png'
],
[
'src' => '/images/icon-512.png',
'sizes' => '512x512',
'type' => 'image/png'
]
]
];
File::put(public_path('manifest.json'), json_encode($manifest, JSON_PRETTY_PRINT));
}
}
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
class PerformanceMonitorService
{
/**
* Record performance metrics
*/
public static function recordMetric($metric, $value, $tags = [])
{
DB::table('performance_metrics')->insert([
'metric' => $metric,
'value' => $value,
'tags' => json_encode($tags),
'created_at' => now(),
]);
}
/**
* Record page load time
*/
public static function recordPageLoad($route, $loadTime, $memoryUsage = null)
{
self::recordMetric('page_load_time', $loadTime, [
'route' => $route,
'memory_usage' => $memoryUsage ?: memory_get_peak_usage(true),
]);
}
/**
* Record database query performance
*/
public static function recordQueryPerformance($query, $time, $bindings = [])
{
self::recordMetric('query_time', $time, [
'query' => $query,
'bindings_count' => count($bindings),
]);
}
/**
* Get performance statistics
*/
public static function getPerformanceStats($hours = 24)
{
$startTime = now()->subHours($hours);
return [
'avg_page_load' => DB::table('performance_metrics')
->where('metric', 'page_load_time')
->where('created_at', '>=', $startTime)
->avg('value'),
'avg_query_time' => DB::table('performance_metrics')
->where('metric', 'query_time')
->where('created_at', '>=', $startTime)
->avg('value'),
'slow_queries' => DB::table('performance_metrics')
->where('metric', 'query_time')
->where('value', '>', 1000) // > 1 second
->where('created_at', '>=', $startTime)
->count(),
'memory_usage' => DB::table('performance_metrics')
->where('metric', 'page_load_time')
->where('created_at', '>=', $startTime)
->selectRaw('AVG(JSON_EXTRACT(tags, "$.memory_usage")) as avg_memory')
->first(),
];
}
/**
* Get slow routes
*/
public static function getSlowRoutes($limit = 10, $hours = 24)
{
$startTime = now()->subHours($hours);
return DB::table('performance_metrics')
->select('tags->route as route', DB::raw('AVG(value) as avg_time'))
->where('metric', 'page_load_time')
->where('created_at', '>=', $startTime)
->groupBy('tags->route')
->orderByDesc('avg_time')
->limit($limit)
->get();
}
/**
* Clean old metrics
*/
public static function cleanOldMetrics($days = 30)
{
return DB::table('performance_metrics')
->where('created_at', '<', now()->subDays($days))
->delete();
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Services\PerformanceMonitorService;
class PerformanceTracker
{
public function handle(Request $request, Closure $next)
{
$startTime = microtime(true);
$startMemory = memory_get_usage(true);
$response = $next($request);
$endTime = microtime(true);
$endMemory = memory_get_usage(true);
$loadTime = ($endTime - $startTime) * 1000; // Convert to milliseconds
$memoryUsed = $endMemory - $startMemory;
// Record metrics
PerformanceMonitorService::recordPageLoad(
$request->route()?->getName() ?: $request->path(),
$loadTime,
$memoryUsed
);
// Add performance headers
$response->headers->set('X-Response-Time', round($loadTime, 2) . 'ms');
$response->headers->set('X-Memory-Usage', round($memoryUsed / 1024 / 1024, 2) . 'MB');
return $response;
}
}
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\PerformanceMonitorService;
class PerformanceReport extends Command
{
protected $signature = 'performance:report {--hours=24 : Hours to analyze}';
protected $description = 'Generate performance report';
public function handle()
{
$hours = $this->option('hours');
$this->info("📊 Performance Report (Last {$hours} hours)");
$this->newLine();
$stats = PerformanceMonitorService::getPerformanceStats($hours);
$this->table(
['Metric', 'Value'],
[
['Average Page Load Time', round($stats['avg_page_load'], 2) . ' ms'],
['Average Query Time', round($stats['avg_query_time'], 2) . ' ms'],
['Slow Queries Count', $stats['slow_queries']],
['Average Memory Usage', round($stats['memory_usage']->avg_memory / 1024 / 1024, 2) . ' MB'],
]
);
$this->newLine();
$this->info('🐌 Slowest Routes:');
$slowRoutes = PerformanceMonitorService::getSlowRoutes(10, $hours);
if ($slowRoutes->isNotEmpty()) {
$this->table(
['Route', 'Average Time (ms)'],
$slowRoutes->map(function($route) {
return [$route->route, round($route->avg_time, 2)];
})->toArray()
);
} else {
$this->line(' No slow routes detected');
}
return 0;
}
}
# Application
APP_ENV=production
APP_DEBUG=false
APP_LOG_LEVEL=error
# Performance
OPCACHE_ENABLED=true
OPCACHE_MEMORY=512
OPCACHE_MAX_FILES=50000
SESSION_GC_PROBABILITY=1
SESSION_GC_DIVISOR=10000
SESSION_GC_MAXLIFETIME=7200
MEMORY_LIMIT=512M
MAX_EXECUTION_TIME=60
# Cache
CACHE_DRIVER=redis
REDIS_CLIENT=phpredis
REDIS_PREFIX=barbershop_prod_
# Queue
QUEUE_CONNECTION=redis
QUEUE_FAILED_DRIVER=database
# Database
DB_CONNECTION=mysql
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
DB_STRICT=true
DB_ENGINE=InnoDB
# Session
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_SECURE_COOKIE=true
# CDN and Assets
ASSET_URL=https://cdn.yourdomain.com
CDN_URL=https://cdn.yourdomain.com
# Image optimization
IMAGE_OPTIMIZATION=true
IMAGE_COMPRESSION_QUALITY=85
WEBP_SUPPORT=true
<?php
return [
'default' => env('CACHE_DRIVER', 'redis'),
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
'serializer' => 'igbinary', // Faster serialization
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/locks'),
],
// APCu for in-memory caching
'apcu' => [
'driver' => 'apcu',
],
// Multi-tier cache
'multi' => [
'driver' => 'array', // Custom driver
'stores' => ['apcu', 'redis', 'file'],
],
],
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
];
Next: Backup & Restore untuk strategi backup dan disaster recovery.