Nuxt 和 Laravel 的 15000 毫秒 TTFB 等待时间

15000ms TTFB waiting time with Nuxt and Laravel

我的 TTFB 时间很长,GET 请求大约需要 15000/17000 毫秒。只有一个特定的调用会发生这种情况,其余的都很好。

我是在添加了 Nuxt Auth 和 Laravel Sanctum 之后才开始遇到这个问题的。 在完成请求并给出 JSON 结果之前,请求保持挂起状态(在调试器网络选项卡下)大约 10 秒

这是我的nuxt.confing.js

export default {
  srcDir: 'resources/nuxt',

  ssr: false,

  head: {
    titleTemplate: '%s - ' + process.env.APP_VERSION,
    title: process.env.APP_NAME || '',
    meta: [
      { charset: 'utf-8' },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1'
      },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      { rel: 'stylesheet', href: 'https://raw.githack.com/lucperkins/bulma-dashboard/master/dist/bulma-dashboard.css' }
      ]
  },

  loading: { color: '#fff' },

  css: [
    '@/assets/main.scss'
  ],

  plugins: [
    "~/plugins/vee-validate.js"
  ],

  components: true,

  buildModules: [
    '@nuxtjs/dotenv',
    '@nuxtjs/eslint-module',
    '@nuxtjs/fontawesome',
    '@nuxtjs/moment',
  ],

  modules: [
    'nuxt-laravel',
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    'nuxt-buefy',
    'nuxt-fontawesome',
    '@nuxtjs/auth-next'
  ],

  build: {
    transpile: [/@fullcalendar.*/,"vee-validate/dist/rules"],
    extend(config, ctx) {
      config.module.rules.push({
        enforce: 'pre',
        test: /\.(js|vue)$/,
        loader: 'eslint-loader',
        exclude: /(node_modules)/,
        options: {
          fix: true
        }
      })
    }
  },

  axios: {
    baseURL: process.env.API_URL,
    debug: true,
    credentials: true
  },
  auth: {
    redirect: {
      login: '/login',
      logout: '/',
      callback: '/login',
      home: '/dashboard/'
    },
    strategies: {
      'laravelSanctum': {
        provider: 'laravel/sanctum',
        url: process.env.API_URL
      }
    },
    localStorage: false
  },

  buefy: {
    materialDesignIcons: false,
    defaultIconPack: 'fas',
    defaultIconComponent: 'font-awesome-icon'
  },

  router: {
    base: '/dashboard/',
    linkActiveClass: 'is-active',
    middleware: ['auth']
  },
  fontawesome: {
      icons: {
        solid: true
      }
  }
}

Nuxt页面(为了方便只放了js代码)

<script>
// https://www.tutsmake.com/laravel-vue-js-full-calendar-example/
import FullCalendar from '@fullcalendar/vue'
import timeGridPlugin from '@fullcalendar/timegrid'
import resourceTimelinePlugin from '@fullcalendar/resource-timeline'

export default {
  components: {
    FullCalendar
  },
  data() {
    return {
      sessions: [],
      todayDisabled: true,
      calTitle: '',
      calendarOptions: {
        plugins: [timeGridPlugin, resourceTimelinePlugin],
        schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives',
        initialView: 'timeGridWeek',
        refetchResourcesOnNavigate: true,
-->>    resources: '/api/sessions', //the very long call
        eventDisplay: 'block',
        contentHeight: 'auto',
        nowIndicator: true,
        locale: 'en-gb',
        timezone: 'Europe/London', // without this, after Daylight Saving Time the event goes 1 hour back
        headerToolbar: false,
        businessHours: [
          {
            daysOfWeek: [1, 2, 3, 4, 5],
            startTime: '08:00',
            endTime: '20:00'
          },
          {
            daysOfWeek: [6],
            startTime: '9:00',
            endTime: '14:00'
          }
        ],
        slotMinTime: '07:00:00',
        slotMaxTime: '24:00:00',

        expandRows: true,
        eventClick: (calendar) => {
          this.$router.push({
            name: 'calendar-id-sessiondate',
            params: {
              id: calendar.event.id,
              sessiondate: this.$moment(calendar.event.start).format(
                'YYYY-MM-DD'
              )
            }
          })
        },
        datesSet: (dateInfo) => {
          this.calTitle = dateInfo.view.title
          this.todayDisabled = this.$moment().isBetween(
            dateInfo.start,
            dateInfo.end
          )
        }
      }
    }
  }
}
</script>

Laravel 控制器 组件“Fullcalendar”通过“resources:'/api/sessions'”运行 GET 请求,该请求转到以下代码。

private function getIntervalTasks($s, $start_period, $end_period)
{
    $sessions = [];

    foreach(CarbonPeriod::create($s->start_datetime, "$s->interval_num $s->interval_type_human", $s->end_datetime) as $start_session) {

      if (Carbon::parse($start_session)->between($start_period, $end_period)) {

        $canceled = false;

        if ($s->exceptions->isNotEmpty()) {
          foreach ($s->exceptions as $e) {
              if (Carbon::parse($e->datetime)->toDateString() === $start_session->toDateString()) {
                if($e->is_canceled) {
                  $canceled = true;
                  break;
                } elseif ($e->is_rescheduled) {
                  $start_session = Carbon::parse($e->datetime);
                }
              }
            }
        }

        if ($canceled) {
          continue;
        }

        $end_session = Carbon::parse($start_session)->addMinutes($s->duration);

        $sessions[] = [
            'id' => (int)$s->id,
            'title' => $s->client->name,
            'start' => $start_session->format('Y-m-d H:i:s'),
            'end' => $end_session->format('Y-m-d H:i:s'),
            'className' => $s->status_colors
          ];
      }
    }

    return $sessions;

}

public function index(Request $request) {
    $start = (!empty($_GET["start"])) ? ($_GET["start"]) : ('');
    $end = (!empty($_GET["end"])) ? ($_GET["end"]) : ('');

    $session_period = SessionPattern::has('client')
      ->where(fn($q)=> $q->whereDate('start_datetime', '<=', $start)->orWhereDate('end_datetime',   '>=', $end)
        ->orWhereBetween(DB::raw('date(`start_datetime`)'), [$start, $end])
        ->with('exceptions', fn($q) => $q->whereBetween(DB::raw('date(`datetime`)'), [$start, $end])
      ))->get();

    $sessions = [];

    foreach ($session_period as $session) {
      if($session->is_recurrent){
        foreach ($this->getIntervalTasks($session, $start, $end) as $s) {
          $sessions[] = $s;
        }

      } else {
        $items = ['none'];
      }
    }

    return response()->json($sessions);
}

ps:我也试过看看是不是Fullcalendar的问题。使用 axios 调用,问题仍然存在。

根据我的类似经历回答你的问题

但为了获得准确的结果,我建议使用 php 分析工具 ,例如 KCachegrind 来找出代码的哪一部分消耗更多的时间来执行。

我认为问题出在,这是我的。

Carbon Object Takes Long time to instantiate (is Slow).

我重构了我的代码,不使用 carbon 进行日期比较(在 DBMS 中进行)并且一切都加快了。

可能的瓶颈

我认为你的问题是,你从数据库中获取了很多记录,并用 foreach 遍历它们:

foreach ($this->getIntervalTasks($session, $start, $end) as $s) {
          $sessions[] = $s;
        }

然后在 getIntervalTasks 你有第二个循环 :

foreach(CarbonPeriod::create($s->start_datetime, "$s->interval_num $s->interval_type_human", $s->end_datetime) as $start_session) {

它按照N*M的顺序执行请求。

Carbon::parse($start_session)->between($start_period, $end_period)

上面的代码Carbon::parse()是N*M(二度)次运行的慢码

可能的解决方案

我认为您的解决方案是避免按此顺序创建碳对象

DBMS 中实现您的业务逻辑(时间比较逻辑)(使用 存储过程DB 函数) 将解决 TTFB 问题。

假设检验

放置这个

$start = microtime(true);
foreach ($session_period as $session){
    ...
}
$time_elapsed_secs = microtime(true) - $start;

return response()->json(array_merge($sessions, $time_elapsed_secs);

您可以使用此技术找到耗时的代码部分。

并且显然测试我的建议是否正确。

注意:返回 $time_elapsed_secs 的时间为 微秒