Sitecoreパフォーマンス最適化の極意:高速化テクニック完全ガイド(Part5)

54分で読めます
エンハンスド編集部
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のパフォーマンス最適化は一度きりの作業ではありません。継続的な監視と改善により、常に最高のパフォーマンスを維持できます。

重要なポイント:

  1. 測定なくして改善なし: 常にパフォーマンスを測定する
  2. キャッシングファースト: 適切なキャッシング戦略が最も効果的
  3. データベースの健全性: 定期的なメンテナンスが必須
  4. フロントエンドも重要: CDNと画像最適化で体感速度向上
  5. 監視とアラート: 問題の早期発見・対応

次回予告:導入事例編

シリーズ最終回となる次回Part6: Sitecore導入事例集では、実際の企業でのSitecore導入事例を詳しく紹介します:

  • 大手小売業での導入事例
  • 金融機関での活用方法
  • グローバル企業での多言語展開
  • ROIと成功要因の分析

実践的な知見をお楽しみに!


関連記事:

技術的な課題をお持ちですか?

記事でご紹介した技術や実装について、
より詳細なご相談やプロジェクトのサポートを承ります

無料技術相談を申し込む