Sitecoreパフォーマンス最適化の極意:高速化テクニック完全ガイド(Part5)
Sitecoreパフォーマンス最適化キャッシングCDN監視
Sitecoreサイトのパフォーマンスを劇的に向上させる実践的な最適化手法を解説。キャッシング戦略、データベース最適化、CDN活用、監視まで、プロダクション環境で実証済みのテクニックを公開します。
はじめに:なぜパフォーマンス最適化が重要なのか
前回の記事(Part4: Sitecore開発実践編)では、Helix原則に基づいた開発手法について学びました。今回は、開発したSitecoreサイトのパフォーマンスを最大限に引き出す方法を解説します。
サイトの表示速度は、ユーザー体験とビジネス成果に直結します。Googleの調査によると:
- ページ読み込みが1秒遅延すると、コンバージョン率が7%低下
- 3秒以上かかると53%のユーザーが離脱
- 表示速度はSEOランキングの重要な要素
この記事では、Sitecoreサイトを高速化する実践的なテクニックを、実際の改善事例とともに解説します。
パフォーマンス診断:現状を正確に把握する
測定ツールの活用
// カスタムパフォーマンスプロファイラー
public class PerformanceProfiler : IDisposable
{
private readonly Stopwatch _stopwatch;
private readonly string _operationName;
private readonly ILogger _logger;
public PerformanceProfiler(string operationName)
{
_operationName = operationName;
_stopwatch = Stopwatch.StartNew();
_logger = LoggerFactory.GetLogger(typeof(PerformanceProfiler));
if (Sitecore.Context.PageMode.IsDebugging)
{
HttpContext.Current.Response.Write($"<!-- Start: {operationName} -->");
}
}
public void Dispose()
{
_stopwatch.Stop();
var duration = _stopwatch.ElapsedMilliseconds;
_logger.Info($"Performance: {_operationName} took {duration}ms");
if (duration > 1000)
{
_logger.Warn($"Slow operation detected: {_operationName} ({duration}ms)");
}
if (Sitecore.Context.PageMode.IsDebugging)
{
HttpContext.Current.Response.Write($"<!-- End: {_operationName} ({duration}ms) -->");
}
// Application Insightsに送信
TelemetryClient.TrackMetric($"Sitecore.{_operationName}.Duration", duration);
}
}
Sitecore Debug Mode の活用
<!-- web.config での設定 -->
<configuration>
<sitecore>
<settings>
<setting name="Counters.Enabled" value="true" />
<setting name="Stats.Enabled" value="true" />
<setting name="Profiling.Enabled" value="true" />
<setting name="DebugMode.ShowProfiler" value="true" />
</settings>
</sitecore>
</configuration>
デバッグ情報のURL: https://yoursite.com/?sc_debug=1&sc_prof=1&sc_trace=1&sc_ri=1
キャッシング戦略:最重要の最適化手法
1. HTML出力キャッシュ
public class SmartHtmlCacheManager
{
private readonly IHtmlCache _htmlCache;
public string GetOrAddHtml(string cacheKey, Func<string> generateHtml, CacheOptions options)
{
// キャッシュキーの生成(バリエーション考慮)
var fullCacheKey = GenerateCacheKey(cacheKey, options);
// キャッシュから取得を試みる
var cachedHtml = _htmlCache.GetHtml(fullCacheKey);
if (!string.IsNullOrEmpty(cachedHtml))
{
Sitecore.Diagnostics.Log.Debug($"Cache hit: {fullCacheKey}");
return cachedHtml;
}
// キャッシュミスの場合は生成
using (new PerformanceProfiler($"Generate HTML for {cacheKey}"))
{
var html = generateHtml();
// キャッシュに保存
_htmlCache.SetHtml(fullCacheKey, html, options.Duration);
return html;
}
}
private string GenerateCacheKey(string baseKey, CacheOptions options)
{
var keyBuilder = new StringBuilder(baseKey);
if (options.VaryByDevice)
keyBuilder.Append($"_device:{GetDeviceId()}");
if (options.VaryByLanguage)
keyBuilder.Append($"_lang:{Sitecore.Context.Language.Name}");
if (options.VaryByUser)
keyBuilder.Append($"_user:{GetUserSegment()}");
if (options.VaryByQueryString)
keyBuilder.Append($"_qs:{GetQueryStringHash()}");
return keyBuilder.ToString();
}
}
2. レンダリングレベルキャッシュ
<!-- レンダリングごとのキャッシュ設定 -->
<configuration>
<sitecore>
<renderingSettings>
<setting name="ProductList">
<Cacheable>true</Cacheable>
<ClearOnIndexUpdate>true</ClearOnIndexUpdate>
<VaryByData>true</VaryByData>
<VaryByDevice>true</VaryByDevice>
<VaryByLanguage>true</VaryByLanguage>
<VaryByParameters>category,page,sort</VaryByParameters>
<VaryByQueryString>false</VaryByQueryString>
<VaryByUser>false</VaryByUser>
<CacheDuration>3600</CacheDuration>
</setting>
</renderingSettings>
</sitecore>
</configuration>
3. カスタムキャッシュの実装
public class DistributedCacheService : ICacheService
{
private readonly IDistributedCache _cache;
private readonly ISerializer _serializer;
public async Task<T> GetOrAddAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan expiration)
{
// Redisから取得を試みる
var cachedData = await _cache.GetAsync(key);
if (cachedData != null)
{
return _serializer.Deserialize<T>(cachedData);
}
// ダブルチェックロッキング
using (await _lockManager.AcquireLockAsync(key, TimeSpan.FromSeconds(30)))
{
// 再度チェック(他のプロセスが生成した可能性)
cachedData = await _cache.GetAsync(key);
if (cachedData != null)
{
return _serializer.Deserialize<T>(cachedData);
}
// データ生成
var data = await factory();
// キャッシュに保存
var serialized = _serializer.Serialize(data);
await _cache.SetAsync(key, serialized, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration,
SlidingExpiration = expiration / 2
});
return data;
}
}
// キャッシュ無効化
public async Task InvalidateAsync(string pattern)
{
var keys = await _cache.GetKeysAsync(pattern);
await Task.WhenAll(keys.Select(key => _cache.RemoveAsync(key)));
}
}
4. キャッシュウォーミング
public class CacheWarmingService : IScheduledTask
{
private readonly IUrlCrawler _crawler;
private readonly ICacheService _cache;
public async Task ExecuteAsync()
{
Log.Info("Cache warming started", this);
// 重要なURLのリストを取得
var urls = GetCriticalUrls();
// 並列でページをリクエスト
var tasks = urls.Select(async url =>
{
try
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
Log.Debug($"Warmed cache for: {url}", this);
}
}
}
catch (Exception ex)
{
Log.Error($"Failed to warm cache for {url}", ex, this);
}
});
await Task.WhenAll(tasks);
Log.Info($"Cache warming completed. Processed {urls.Count} URLs", this);
}
private List<string> GetCriticalUrls()
{
return new List<string>
{
"/",
"/products",
"/products/category/popular",
"/about",
"/contact"
};
}
}
データベース最適化
1. インデックス戦略
-- パフォーマンスが悪いクエリの特定
SELECT TOP 20
qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time,
qs.execution_count,
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2) + 1) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qt.dbid = DB_ID('Sitecore_Master')
ORDER BY avg_elapsed_time DESC;
-- 推奨インデックスの作成
CREATE NONCLUSTERED INDEX IX_Items_TemplateID_Language
ON Items (TemplateID, Language)
INCLUDE (Name, Created, Updated)
WHERE TemplateID IS NOT NULL;
CREATE NONCLUSTERED INDEX IX_Items_ParentID_Name
ON Items (ParentID, Name)
INCLUDE (TemplateID, Language);
2. クエリ最適化
public class OptimizedItemRepository
{
// ❌ 悪い例:N+1問題
public List<ProductInfo> GetProductsBad(ID categoryId)
{
var products = new List<ProductInfo>();
var categoryItem = Database.GetItem(categoryId);
foreach (Item child in categoryItem.Children)
{
// 各アイテムごとにDBアクセスが発生
var product = new ProductInfo
{
Name = child["Product Name"],
Price = child["Price"],
// 関連アイテムの取得でさらにDBアクセス
Category = Database.GetItem(child["Category"])?.Name,
Brand = Database.GetItem(child["Brand"])?.Name
};
products.Add(product);
}
return products;
}
// ✅ 良い例:バッチ処理
public List<ProductInfo> GetProductsOptimized(ID categoryId)
{
using (var context = ContentSearchManager.GetIndex("sitecore_web_index").CreateSearchContext())
{
var results = context.GetQueryable<SearchResultItem>()
.Where(item => item.Paths.Contains(categoryId))
.Where(item => item.TemplateId == ProductTemplateId)
.Select(item => new ProductInfo
{
Name = item["Product Name"],
Price = item["Price"],
Category = item["Category_t"],
Brand = item["Brand_t"]
})
.Take(100)
.ToList();
return results;
}
}
}
3. データベースメンテナンス
-- 定期メンテナンススクリプト
-- 統計情報の更新
UPDATE STATISTICS [dbo].[Items] WITH FULLSCAN;
UPDATE STATISTICS [dbo].[SharedFields] WITH FULLSCAN;
UPDATE STATISTICS [dbo].[UnversionedFields] WITH FULLSCAN;
UPDATE STATISTICS [dbo].[VersionedFields] WITH FULLSCAN;
-- インデックスの再構築(断片化が30%以上の場合)
DECLARE @TableName VARCHAR(255)
DECLARE @IndexName VARCHAR(255)
DECLARE @FragmentationPercent FLOAT
DECLARE index_cursor CURSOR FOR
SELECT
t.name AS TableName,
i.name AS IndexName,
s.avg_fragmentation_in_percent
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') s
JOIN sys.indexes i ON s.object_id = i.object_id AND s.index_id = i.index_id
JOIN sys.tables t ON i.object_id = t.object_id
WHERE s.avg_fragmentation_in_percent > 30 AND i.name IS NOT NULL
OPEN index_cursor
FETCH NEXT FROM index_cursor INTO @TableName, @IndexName, @FragmentationPercent
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT 'Rebuilding index ' + @IndexName + ' on table ' + @TableName + ' (Fragmentation: ' + CAST(@FragmentationPercent AS VARCHAR(10)) + '%)'
EXEC('ALTER INDEX [' + @IndexName + '] ON [' + @TableName + '] REBUILD')
FETCH NEXT FROM index_cursor INTO @TableName, @IndexName, @FragmentationPercent
END
CLOSE index_cursor
DEALLOCATE index_cursor
CDN統合とスタティックアセット最適化
1. Azure CDNの設定
public class CdnUrlProvider : IMediaUrlProvider
{
private readonly string _cdnBaseUrl;
private readonly bool _useCdn;
public CdnUrlProvider(IConfiguration config)
{
_cdnBaseUrl = config["Sitecore:CDN:BaseUrl"];
_useCdn = config.GetValue<bool>("Sitecore:CDN:Enabled");
}
public string GetMediaUrl(MediaItem mediaItem, MediaUrlOptions options)
{
var baseUrl = MediaManager.GetMediaUrl(mediaItem, options);
if (!_useCdn || Context.PageMode.IsExperienceEditorEditing)
{
return baseUrl;
}
// CDN URLに変換
var cdnUrl = baseUrl.Replace("/~/media", $"{_cdnBaseUrl}/media");
// バージョニング追加(キャッシュバスティング)
var version = GetMediaVersion(mediaItem);
cdnUrl += $"?v={version}";
return cdnUrl;
}
private string GetMediaVersion(MediaItem mediaItem)
{
// 更新日時のハッシュを使用
var updated = mediaItem.InnerItem.Statistics.Updated;
return updated.Ticks.ToString("x8");
}
}
2. 画像最適化
public class ImageOptimizationProcessor : GetMediaStreamProcessor
{
public override void Process(GetMediaStreamArgs args)
{
if (!IsImage(args.MediaData.MimeType))
return;
var options = args.Options;
// レスポンシブ画像の生成
if (options.CustomOptions.ContainsKey("responsive"))
{
GenerateResponsiveImages(args);
return;
}
// WebP変換
if (SupportsWebP(args.HttpContext))
{
ConvertToWebP(args);
return;
}
// 画質最適化
OptimizeImageQuality(args);
}
private void GenerateResponsiveImages(GetMediaStreamArgs args)
{
var sizes = new[] { 320, 640, 1024, 1920 };
var srcset = new List<string>();
foreach (var width in sizes)
{
var url = MediaManager.GetMediaUrl(args.MediaData.MediaItem, new MediaUrlOptions
{
Width = width,
AllowStretch = false
});
srcset.Add($"{url} {width}w");
}
// srcset属性用の文字列を生成
args.Options.CustomOptions["srcset"] = string.Join(", ", srcset);
}
}
3. JavaScript/CSSの最適化
public class AssetOptimizationModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.PreRequestHandlerExecute += OnPreRequestHandlerExecute;
}
private void OnPreRequestHandlerExecute(object sender, EventArgs e)
{
var context = HttpContext.Current;
var path = context.Request.Path.ToLower();
if (path.EndsWith(".js") || path.EndsWith(".css"))
{
// 圧縮
context.Response.Filter = new GZipStream(
context.Response.Filter,
CompressionMode.Compress);
// キャッシュヘッダー
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetExpires(DateTime.UtcNow.AddYears(1));
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(365));
// ETag設定
var etag = GetFileETag(context.Request.PhysicalPath);
context.Response.Cache.SetETag(etag);
}
}
}
モニタリングとアラート
1. Application Insights統合
public class SitecoreInsightsModule : IHttpModule
{
private readonly TelemetryClient _telemetryClient;
public void Init(HttpApplication context)
{
context.BeginRequest += OnBeginRequest;
context.EndRequest += OnEndRequest;
context.Error += OnError;
}
private void OnBeginRequest(object sender, EventArgs e)
{
var context = HttpContext.Current;
var stopwatch = Stopwatch.StartNew();
context.Items["RequestStopwatch"] = stopwatch;
// カスタムプロパティの追加
var properties = new Dictionary<string, string>
{
["SitecoreItem"] = Sitecore.Context.Item?.Paths.FullPath,
["SitecoreLanguage"] = Sitecore.Context.Language?.Name,
["SitecoreDatabase"] = Sitecore.Context.Database?.Name,
["SitecoreUser"] = Sitecore.Context.User?.Name
};
context.Items["TelemetryProperties"] = properties;
}
private void OnEndRequest(object sender, EventArgs e)
{
var context = HttpContext.Current;
var stopwatch = context.Items["RequestStopwatch"] as Stopwatch;
var properties = context.Items["TelemetryProperties"] as Dictionary<string, string>;
if (stopwatch != null && properties != null)
{
stopwatch.Stop();
// パフォーマンスメトリクスの記録
_telemetryClient.TrackMetric("RequestDuration", stopwatch.ElapsedMilliseconds, properties);
// 遅いリクエストの警告
if (stopwatch.ElapsedMilliseconds > 3000)
{
_telemetryClient.TrackEvent("SlowRequest", properties, new Dictionary<string, double>
{
["Duration"] = stopwatch.ElapsedMilliseconds
});
}
}
}
}
2. カスタムヘルスチェック
[Route("api/health")]
public class HealthCheckController : ApiController
{
[HttpGet]
public async Task<IHttpActionResult> GetHealth()
{
var checks = new List<HealthCheckResult>();
// データベース接続チェック
checks.Add(await CheckDatabase());
// Solrインデックスチェック
checks.Add(await CheckSolr());
// キャッシュサーバーチェック
checks.Add(await CheckRedis());
// ディスク容量チェック
checks.Add(CheckDiskSpace());
// メモリ使用率チェック
checks.Add(CheckMemoryUsage());
var isHealthy = checks.All(c => c.Status == HealthStatus.Healthy);
var status = isHealthy ? HttpStatusCode.OK : HttpStatusCode.ServiceUnavailable;
return Content(status, new
{
Status = isHealthy ? "Healthy" : "Unhealthy",
Timestamp = DateTime.UtcNow,
Checks = checks,
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString()
});
}
private async Task<HealthCheckResult> CheckDatabase()
{
try
{
using (var db = Factory.GetDatabase("master"))
{
var item = db.GetItem("/sitecore");
return new HealthCheckResult
{
Name = "Database",
Status = item != null ? HealthStatus.Healthy : HealthStatus.Unhealthy,
ResponseTime = 0
};
}
}
catch (Exception ex)
{
return new HealthCheckResult
{
Name = "Database",
Status = HealthStatus.Unhealthy,
Error = ex.Message
};
}
}
}
実践的なパフォーマンス改善事例
Before: 問題のあるコード
public ActionResult SlowProductList()
{
// 問題1: キャッシュなし
var products = new List<Product>();
// 問題2: 非効率なクエリ
var productFolder = Sitecore.Context.Database.GetItem("/sitecore/content/products");
foreach (Item child in productFolder.Children)
{
// 問題3: N+1クエリ
var product = new Product
{
Name = child.Name,
Price = decimal.Parse(child["Price"]),
Category = child.Database.GetItem(child["CategoryId"])?.Name,
InStock = GetStockLevel(child.ID) > 0
};
products.Add(product);
}
// 問題4: ビューステートが大きい
ViewBag.AllProducts = products;
ViewBag.Categories = GetAllCategories();
ViewBag.Brands = GetAllBrands();
return View(products);
}
After: 最適化されたコード
public ActionResult OptimizedProductList()
{
return _cache.GetOrAdd($"products_{Context.Language.Name}", () =>
{
using (var context = ContentSearchManager.GetIndex("sitecore_web_index").CreateSearchContext())
{
// 最適化されたクエリ
var products = context.GetQueryable<ProductSearchResult>()
.Where(p => p.TemplateId == ProductTemplateId)
.Where(p => p.Language == Context.Language.Name)
.Select(p => new ProductViewModel
{
Id = p.ItemId,
Name = p.Name,
Price = p.Price,
Category = p.Category,
InStock = p.StockLevel > 0,
Url = p.Url
})
.Take(50)
.ToList();
return View(products);
}
}, TimeSpan.FromMinutes(15));
}
改善結果
Performance Improvements:
Page Load Time:
Before: 4.2s
After: 0.8s
Improvement: 81%
Database Queries:
Before: 152 queries/page
After: 3 queries/page
Reduction: 98%
Memory Usage:
Before: 450MB/request
After: 32MB/request
Reduction: 93%
Throughput:
Before: 50 req/sec
After: 500 req/sec
Improvement: 10x
パフォーマンスチェックリスト
開発時のチェック項目
- すべてのレンダリングにキャッシュが設定されているか
- 検索にはContentSearchを使用しているか
- N+1クエリが発生していないか
- 大きなViewStateを避けているか
- 画像の最適化が行われているか
- JavaScriptは非同期で読み込まれているか
- CSSは最小化されているか
- 不要なHTTPリクエストを削減したか
デプロイ前のチェック項目
- パフォーマンステストを実施したか
- キャッシュウォーミングが設定されているか
- CDNが正しく設定されているか
- データベースインデックスが最適化されているか
- エラーログに警告がないか
- モニタリングが設定されているか
まとめ:継続的な最適化が鍵
Sitecoreのパフォーマンス最適化は一度きりの作業ではありません。継続的な監視と改善により、常に最高のパフォーマンスを維持できます。
重要なポイント:
- 測定なくして改善なし: 常にパフォーマンスを測定する
- キャッシングファースト: 適切なキャッシング戦略が最も効果的
- データベースの健全性: 定期的なメンテナンスが必須
- フロントエンドも重要: CDNと画像最適化で体感速度向上
- 監視とアラート: 問題の早期発見・対応
次回予告:導入事例編
シリーズ最終回となる次回Part6: Sitecore導入事例集では、実際の企業でのSitecore導入事例を詳しく紹介します:
- 大手小売業での導入事例
- 金融機関での活用方法
- グローバル企業での多言語展開
- ROIと成功要因の分析
実践的な知見をお楽しみに!
関連記事: