Performance Optimization - luckydeva03/desa_karangrejo GitHub Wiki
Panduan lengkap untuk mengoptimalkan performa Website Desa Karangrejo agar lebih cepat dan efisien.
Performance optimization adalah proses meningkatkan kecepatan dan efisiensi website. Panduan ini mencakup optimasi di semua level: aplikasi, database, server, dan network.
- Largest Contentful Paint (LCP): < 2.5 detik
- First Input Delay (FID): < 100 milidetik
- Cumulative Layout Shift (CLS): < 0.1
- Time to First Byte (TTFB): < 800 milidetik
- Page Load Time: < 3 detik
- Page Size: < 2MB
- HTTP Requests: < 50 per halaman
- Database Queries: < 20 per halaman
// app/Http/Controllers/Admin/MediaController.php
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;
class MediaController extends Controller
{
public function uploadImage(Request $request)
{
$request->validate([
'image' => 'required|image|max:5120' // 5MB max
]);
$file = $request->file('image');
$manager = new ImageManager(new Driver());
// Create different sizes
$sizes = [
'original' => ['width' => 1920, 'quality' => 85],
'large' => ['width' => 1200, 'quality' => 80],
'medium' => ['width' => 800, 'quality' => 75],
'small' => ['width' => 400, 'quality' => 70],
'thumbnail' => ['width' => 150, 'quality' => 65]
];
$filename = Str::random(40);
$savedFiles = [];
foreach ($sizes as $sizeName => $config) {
$image = $manager->read($file->getPathname());
// Resize maintaining aspect ratio
$image->scaleDown(width: $config['width']);
// Convert to WebP for better compression
$webpFilename = "{$filename}_{$sizeName}.webp";
$webpPath = storage_path("app/public/images/{$webpFilename}");
$image->toWebp($config['quality'])->save($webpPath);
// Fallback JPEG
$jpegFilename = "{$filename}_{$sizeName}.jpg";
$jpegPath = storage_path("app/public/images/{$jpegFilename}");
$image->toJpeg($config['quality'])->save($jpegPath);
$savedFiles[$sizeName] = [
'webp' => "images/{$webpFilename}",
'jpeg' => "images/{$jpegFilename}"
];
}
return response()->json([
'success' => true,
'files' => $savedFiles
]);
}
}
{{-- Responsive image component --}}
<picture class="block w-full">
<source
srcset="{{ Storage::url($image['webp']) }}"
type="image/webp"
media="(min-width: 1024px)"
>
<source
srcset="{{ Storage::url($imageMedium['webp']) }}"
type="image/webp"
media="(min-width: 768px)"
>
<source
srcset="{{ Storage::url($imageSmall['webp']) }}"
type="image/webp"
media="(max-width: 767px)"
>
{{-- Fallback for browsers that don't support WebP --}}
<img
src="{{ Storage::url($imageMedium['jpeg']) }}"
alt="{{ $alt }}"
class="w-full h-auto"
loading="lazy"
decoding="async"
>
</picture>
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js'
],
refresh: true,
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['alpinejs'],
utils: ['sweetalert2', 'chart.js']
}
}
},
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
css: {
postcss: {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
require('cssnano')({
preset: 'default'
})
]
}
}
});
// app/View/Components/CriticalCss.php
class CriticalCss extends Component
{
public function render()
{
$criticalCss = $this->extractCriticalCss();
return view('components.critical-css', [
'criticalCss' => $criticalCss
]);
}
private function extractCriticalCss()
{
// Extract critical CSS for above-the-fold content
$criticalStyles = [
// Header styles
'.header { background: #1f2937; color: white; }',
'.logo { height: 60px; }',
// Navigation styles
'.nav-link { color: white; padding: 1rem; }',
// Hero section styles
'.hero { background: linear-gradient(...); min-height: 50vh; }',
// Layout styles
'.container { max-width: 1200px; margin: 0 auto; }',
];
return implode("\n", $criticalStyles);
}
}
{{-- In layout head --}}
<style>
{!! $criticalCss !!}
</style>
{{-- Defer non-critical CSS --}}
<link rel="preload" href="{{ Vite::asset('resources/css/app.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ Vite::asset('resources/css/app.css') }}"></noscript>
// resources/js/lazy-loading.js
class LazyLoader {
constructor() {
this.imageObserver = null;
this.contentObserver = null;
this.init();
}
init() {
if ('IntersectionObserver' in window) {
this.setupImageLazyLoading();
this.setupContentLazyLoading();
} else {
this.loadAllImages();
}
}
setupImageLazyLoading() {
this.imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
img.removeAttribute('data-srcset');
}
img.classList.remove('lazy');
this.imageObserver.unobserve(img);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.1
});
document.querySelectorAll('img[data-src]').forEach(img => {
this.imageObserver.observe(img);
});
}
setupContentLazyLoading() {
this.contentObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
const url = element.dataset.lazyUrl;
if (url) {
this.loadContent(element, url);
this.contentObserver.unobserve(element);
}
}
});
});
document.querySelectorAll('[data-lazy-url]').forEach(el => {
this.contentObserver.observe(el);
});
}
async loadContent(element, url) {
try {
const response = await fetch(url);
const html = await response.text();
element.innerHTML = html;
element.classList.add('loaded');
} catch (error) {
console.error('Failed to load content:', error);
element.innerHTML = '<p>Failed to load content</p>';
}
}
loadAllImages() {
document.querySelectorAll('img[data-src]').forEach(img => {
img.src = img.dataset.src;
img.removeAttribute('data-src');
});
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new LazyLoader();
});
// ❌ Bad - N+1 queries
$posts = Post::all();
foreach ($posts as $post) {
echo $post->category->name; // N+1 query problem
echo $post->user->name;
}
// ✅ Good - Eager loading
$posts = Post::with(['category', 'user'])->get();
foreach ($posts as $post) {
echo $post->category->name;
echo $post->user->name;
}
// ✅ Better - Selective eager loading
$posts = Post::with(['category:id,name', 'user:id,name'])
->select('id', 'title', 'category_id', 'user_id', 'created_at')
->get();
// ✅ Best - Chunking for large datasets
Post::with(['category', 'user'])
->chunk(100, function ($posts) {
foreach ($posts as $post) {
// Process post
}
});
// High-performance post listing
class PostRepository
{
public function getOptimizedPosts($page = 1, $perPage = 12)
{
return DB::table('posts as p')
->select([
'p.id',
'p.title',
'p.slug',
'p.excerpt',
'p.featured_image',
'p.published_at',
'p.views',
'c.name as category_name',
'c.slug as category_slug',
'u.name as author_name'
])
->join('categories as c', 'p.category_id', '=', 'c.id')
->join('users as u', 'p.user_id', '=', 'u.id')
->where('p.status', 'published')
->where('p.published_at', '<=', now())
->orderBy('p.published_at', 'desc')
->offset(($page - 1) * $perPage)
->limit($perPage)
->get();
}
public function getPopularPosts($limit = 5)
{
return Cache::remember('popular_posts', 3600, function () use ($limit) {
return DB::table('posts')
->select('id', 'title', 'slug', 'views', 'featured_image')
->where('status', 'published')
->where('published_at', '>=', now()->subMonths(3))
->orderBy('views', 'desc')
->limit($limit)
->get();
});
}
}
-- Posts table indexes for common queries
CREATE INDEX idx_posts_status_published ON posts(status, published_at DESC);
CREATE INDEX idx_posts_category_status ON posts(category_id, status, published_at DESC);
CREATE INDEX idx_posts_user_status ON posts(user_id, status, published_at DESC);
CREATE INDEX idx_posts_featured ON posts(is_featured, status, published_at DESC);
CREATE INDEX idx_posts_views ON posts(views DESC);
-- Full-text search indexes
CREATE FULLTEXT INDEX idx_posts_search ON posts(title, excerpt, content);
CREATE FULLTEXT INDEX idx_pages_search ON pages(title, content);
-- Gallery indexes
CREATE INDEX idx_galleries_category ON galleries(category_id, is_active);
CREATE INDEX idx_galleries_active_sort ON galleries(is_active, sort_order);
-- Activity log indexes for performance
CREATE INDEX idx_activity_log_subject ON activity_log(subject_type, subject_id);
CREATE INDEX idx_activity_log_causer ON activity_log(causer_type, causer_id);
CREATE INDEX idx_activity_log_created ON activity_log(created_at DESC);
-- User and authentication indexes
CREATE INDEX idx_users_email_active ON users(email, is_active);
CREATE INDEX idx_users_last_login ON users(last_login_at DESC);
-- Settings cache index
CREATE INDEX idx_settings_key_group ON settings(`key`, `group`);
[mysqld]
# Basic Settings
default_storage_engine = InnoDB
default_table_type = InnoDB
# Connection Settings
max_connections = 200
max_connect_errors = 1000
thread_cache_size = 50
table_open_cache = 4000
# InnoDB Settings
innodb_buffer_pool_size = 2G # 70-80% of available RAM
innodb_log_file_size = 256M
innodb_log_buffer_size = 64M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 120
innodb_thread_concurrency = 0
innodb_file_per_table = 1
# Query Cache (use carefully with InnoDB)
query_cache_type = 1
query_cache_size = 256M
query_cache_limit = 2M
# Slow Query Log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
log_queries_not_using_indexes = 1
# Binary Logging
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
# Charset
character_set_server = utf8mb4
collation_server = utf8mb4_unicode_ci
# Security
sql_mode = STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO
# Performance Schema
performance_schema = ON
// config/database.php - Redis optimization
'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', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'read_write_timeout' => 60,
'context' => [
'auth' => env('REDIS_PASSWORD'),
],
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
];
// app/Services/CacheService.php
class CacheService
{
const CACHE_TTL = [
'posts' => 3600, // 1 hour
'pages' => 7200, // 2 hours
'categories' => 86400, // 24 hours
'settings' => 86400, // 24 hours
'gallery' => 1800, // 30 minutes
'stats' => 300, // 5 minutes
];
public function rememberPosts($key, $callback, $ttl = null)
{
$ttl = $ttl ?? self::CACHE_TTL['posts'];
$cacheKey = "posts:{$key}";
return Cache::remember($cacheKey, $ttl, $callback);
}
public function forgetPostsCache()
{
$pattern = "posts:*";
$keys = Redis::keys($pattern);
if (!empty($keys)) {
Redis::del($keys);
}
}
public function cacheHomepageData()
{
return Cache::remember('homepage_data', 1800, function () {
return [
'featured_posts' => Post::featured()->published()->limit(3)->get(),
'latest_posts' => Post::published()->latest()->limit(6)->get(),
'announcements' => Announcement::active()->limit(3)->get(),
'village_stats' => VillageData::active()->orderBy('sort_order')->get(),
'latest_gallery' => Gallery::active()->latest()->limit(8)->get(),
];
});
}
public function warmupCache()
{
// Preload frequently accessed data
$this->cacheHomepageData();
Cache::remember('categories_menu', 86400, function () {
return Category::active()->orderBy('sort_order')->get();
});
Cache::remember('site_settings', 86400, function () {
return Setting::pluck('value', 'key')->toArray();
});
}
}
# /etc/nginx/conf.d/cache.conf
# Cache zones
proxy_cache_path /var/cache/nginx/static levels=1:2 keys_zone=static_cache:100m max_size=1g inactive=7d use_temp_path=off;
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=api_cache:50m max_size=500m inactive=1h use_temp_path=off;
# Static files caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
access_log off;
# Enable Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json
image/svg+xml;
}
# API caching
location /api/ {
proxy_cache api_cache;
proxy_cache_key $request_uri;
proxy_cache_valid 200 5m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status;
# Pass to PHP-FPM
try_files $uri $uri/ /index.php?$query_string;
}
# Page caching for anonymous users
location / {
set $skip_cache 0;
# Skip cache for logged-in users
if ($http_cookie ~* "laravel_session") {
set $skip_cache 1;
}
# Skip cache for admin pages
if ($request_uri ~* "/admin") {
set $skip_cache 1;
}
proxy_cache static_cache;
proxy_cache_bypass $skip_cache;
proxy_no_cache $skip_cache;
proxy_cache_key $request_uri;
proxy_cache_valid 200 10m;
add_header X-Cache-Status $upstream_cache_status;
try_files $uri $uri/ /index.php?$query_string;
}
; /etc/php/8.2/fpm/pool.d/desa-karangrejo.conf
[desa-karangrejo]
user = www-data
group = www-data
listen = /run/php/php8.2-fpm-desa.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
; Process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 15
pm.max_requests = 1000
; Performance tuning
request_terminate_timeout = 300
request_slowlog_timeout = 30
slowlog = /var/log/php8.2-fpm-slow.log
; Monitoring
pm.status_path = /fpm-status
ping.path = /fpm-ping
; Security
security.limit_extensions = .php
; Environment variables
env[HOSTNAME] = $HOSTNAME
env[PATH] = /usr/local/bin:/usr/bin:/bin
env[TMP] = /tmp
env[TMPDIR] = /tmp
env[TEMP] = /tmp
; /etc/php/8.2/fpm/conf.d/10-opcache.ini
zend_extension=opcache.so
; Enable OPcache
opcache.enable=1
opcache.enable_cli=0
; Memory settings
opcache.memory_consumption=512
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=20000
; Performance settings
opcache.validate_timestamps=0 ; Disable in production
opcache.revalidate_freq=0
opcache.save_comments=1
opcache.fast_shutdown=1
; File cache (optional)
opcache.file_cache=/var/cache/opcache
opcache.file_cache_only=0
; Optimization
opcache.optimization_level=0x7FFFBFFF
opcache.enable_file_override=1
opcache.dups_fix=1
opcache.max_file_size=0
; Error handling
opcache.log_verbosity_level=2
opcache.preferred_memory_model=mmap
# /etc/sysctl.conf
# Network optimization
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 65536 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.core.netdev_max_backlog = 5000
net.ipv4.tcp_congestion_control = bbr
# File system optimization
fs.file-max = 65535
fs.inotify.max_user_watches = 524288
# Memory management
vm.swappiness = 10
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
# Apply changes
sysctl -p
// app/Http/Middleware/PerformanceMonitor.php
class PerformanceMonitor
{
public function handle($request, Closure $next)
{
$startTime = microtime(true);
$startMemory = memory_get_usage(true);
$response = $next($request);
$endTime = microtime(true);
$endMemory = memory_get_usage(true);
$duration = round(($endTime - $startTime) * 1000, 2); // ms
$memoryUsed = round(($endMemory - $startMemory) / 1024 / 1024, 2); // MB
// Log slow requests
if ($duration > 1000) { // > 1 second
Log::warning('Slow request detected', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'duration' => $duration . 'ms',
'memory' => $memoryUsed . 'MB',
'user_id' => auth()->id(),
'ip' => $request->ip()
]);
}
// Add performance headers (development only)
if (config('app.debug')) {
$response->headers->add([
'X-Response-Time' => $duration . 'ms',
'X-Memory-Usage' => $memoryUsed . 'MB',
'X-DB-Queries' => DB::getQueryLog() ? count(DB::getQueryLog()) : 0
]);
}
return $response;
}
}
// resources/js/performance-monitor.js
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// Wait for page load
window.addEventListener('load', () => {
this.collectMetrics();
this.sendMetrics();
});
// Collect Core Web Vitals
this.collectCoreWebVitals();
}
collectMetrics() {
const navigation = performance.getEntriesByType('navigation')[0];
this.metrics = {
// Basic timing
dns_lookup: navigation.domainLookupEnd - navigation.domainLookupStart,
tcp_connect: navigation.connectEnd - navigation.connectStart,
ssl_negotiation: navigation.connectEnd - navigation.secureConnectionStart,
ttfb: navigation.responseStart - navigation.requestStart,
download: navigation.responseEnd - navigation.responseStart,
dom_ready: navigation.domContentLoadedEventEnd - navigation.navigationStart,
page_load: navigation.loadEventEnd - navigation.navigationStart,
// Resource counts
total_resources: performance.getEntriesByType('resource').length,
// Page info
url: window.location.href,
user_agent: navigator.userAgent,
viewport: window.innerWidth + 'x' + window.innerHeight,
timestamp: Date.now()
};
// Memory usage (if available)
if (performance.memory) {
this.metrics.memory_used = performance.memory.usedJSHeapSize;
this.metrics.memory_total = performance.memory.totalJSHeapSize;
}
}
collectCoreWebVitals() {
// Largest Contentful Paint
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
this.metrics.lcp = lastEntry.startTime;
}).observe({entryTypes: ['largest-contentful-paint']});
// First Input Delay
new PerformanceObserver((entryList) => {
const firstInput = entryList.getEntries()[0];
this.metrics.fid = firstInput.processingStart - firstInput.startTime;
}).observe({entryTypes: ['first-input']});
// Cumulative Layout Shift
let clsScore = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
clsScore += entry.value;
}
}
this.metrics.cls = clsScore;
}).observe({entryTypes: ['layout-shift']});
}
sendMetrics() {
// Send to analytics endpoint
fetch('/api/analytics/performance', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify(this.metrics)
}).catch(error => {
console.error('Failed to send performance metrics:', error);
});
}
}
// Initialize performance monitoring
document.addEventListener('DOMContentLoaded', () => {
new PerformanceMonitor();
});
// app/Http/Controllers/Admin/PerformanceController.php
class PerformanceController extends Controller
{
public function dashboard()
{
$metrics = $this->getPerformanceMetrics();
return view('admin.performance.dashboard', compact('metrics'));
}
private function getPerformanceMetrics()
{
$lastWeek = now()->subWeek();
return [
'avg_response_time' => DB::table('performance_logs')
->where('created_at', '>', $lastWeek)
->avg('response_time'),
'slow_queries' => DB::table('performance_logs')
->where('created_at', '>', $lastWeek)
->where('response_time', '>', 1000)
->count(),
'error_rate' => DB::table('performance_logs')
->where('created_at', '>', $lastWeek)
->where('status_code', '>=', 400)
->count() / DB::table('performance_logs')->where('created_at', '>', $lastWeek)->count() * 100,
'top_slow_pages' => DB::table('performance_logs')
->select('url', DB::raw('AVG(response_time) as avg_time'), DB::raw('COUNT(*) as requests'))
->where('created_at', '>', $lastWeek)
->groupBy('url')
->orderBy('avg_time', 'desc')
->limit(10)
->get(),
];
}
}
Performance optimization adalah proses berkelanjutan yang memerlukan monitoring dan fine-tuning terus-menerus ⚡