phplaravelnuxt.jsfullcalendarlaravel-sanctum

15000ms TTFB waiting time with Nuxt and Laravel


I am experiencing a very long TTFB time, around 15000/17000ms with a GET request. This is happening only with one specific call, the rest are fine.

I started experiencing this only after adding Nuxt Auth and Laravel Sanctum. The request remains in pending (under the debugger network tab) for around 10 seconds before completing the request and giving the JSON result.

Here is my 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 page (I put only the js code for convenience)

<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 Controller The component "Fullcalendar" runs a GET request through "resources: '/api/sessions'" which goes to the following code.

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: I also tried to see if the problem was with Fullcalendar. With a axios call, the issue continues.


Solution

  • I am answering your question based on my similar experience.

    But for accurate result i suggest to use php profiling tools like KCachegrind to find out which part of your code consumes more time to execute.

    i Think The problem is With Carbon Which was Mine.

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

    i have refactored my code to not use carbon for date compare (make it in DBMS) and everything speeds up.

    The Probable Bottle Neck

    I Think Your Problem is, you have fetched a lot of records from DB, and loop over them with foreach :

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

    then in getIntervalTasks you have a second loop :

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

    it make the request execution in order of N*M .

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

    the above code Carbon::parse() is the slow code which is running in N*M (second degree) times.

    Probable Solution

    I Think Your Solution will be to Avoid Creating Carbon Object In This Order.

    Implement Your Business Logic (Time Compare Logic) In DBMS (With Store Procedure Or DB Function) Would Solve The TTFB Issue.

    Hypothesis Test

    place this

    $start = microtime(true);
    foreach ($session_period as $session){
        ...
    }
    $time_elapsed_secs = microtime(true) - $start;
    
    return response()->json(array_merge($sessions, $time_elapsed_secs);
    

    you can find the time consuming code section using this technique.

    and obviously test if my suggestion was correct or not.

    NOTE: returning $time_elapsed_secs is in micro second.